概述
索引是关系型数据库中最为核心的性能优化手段。对于查询操作而言,索引如同书籍的目录,能够帮助数据库快速定位到目标数据所在的位置,而无需逐页翻阅整本"书"。在没有索引的情况下,PostgreSQL 必须执行顺序扫描(Sequential Scan)------即逐行遍历整个表,直到找到匹配的行。随着表数据量的增长,这种扫描方式的效率呈线性下降。
本文将从索引的基本概念、常见类型、创建语法及注意事项入手,帮助读者建立索引知识的基础框架。后续文章将深入探讨索引的底层实现原理(如 B-Tree 结构、页分裂机制)以及更高级的优化策略。
索引的作用与性能对比
顺序扫描 vs. 索引扫描
在没有索引的情况下,PostgreSQL 对表的查询只能采用顺序扫描方式,即从表的第一个数据块开始,逐行读取直至末尾。这种方式的时间复杂度为 O ( n ) O(n) O(n),其中 n n n 为表的数据行数。
而有了索引之后,查询优化器可以选择索引扫描(Index Scan) ,通过索引结构快速定位到目标行的物理存储位置(即 CTID,包含数据块编号与块内偏移量),从而将查询复杂度降低至 O ( log n ) O(\log n) O(logn) 级别。
性能对比演示
以下通过一个简单的实验来直观感受索引带来的性能提升:
sql
-- 开启查询计时
\timing on
-- 创建测试表
CREATE TABLE test_index (
id INT
);
-- 插入100万行数据(利用 generate_series)
INSERT INTO test_index (id)
SELECT generate_series(1, 1000000);
-- 无索引时的查询(顺序扫描)
SELECT * FROM test_index WHERE id = 99999;
-- 执行时间示例:约 55 ms(取决于硬件配置)
此时表中尚无索引,PostgreSQL 只能进行顺序扫描。接下来创建索引并再次执行相同的查询:
sql
-- 创建 B-Tree 索引(默认索引类型)
CREATE INDEX idx_test_index_id ON test_index (id);
-- 再次执行查询
SELECT * FROM test_index WHERE id = 99999;
-- 执行时间示例:约 0.5 ms
从 55 ms 降至 0.5 ms,性能提升超过 100 倍。随着数据量的进一步增长和查询条件的复杂化,这一差距会更加显著。
CTID:索引与数据行的纽带
PostgreSQL 中每个表行都有一个隐式的系统列 CTID,它记录了该行所在的数据块编号(Block Number) 与块内偏移量(Offset) 。索引中存储的并非完整的数据行,而是"索引键值 + 对应的 CTID"这样一个键值对。当查询通过索引找到匹配的键值后,数据库便可根据 CTID 直接定位到数据行的物理存储位置,从而完成数据的检索。
索引类型
PostgreSQL 提供了多种索引方法(Index Methods),包括 B-tree、Hash、GiST、SP-GiST、GIN、BRIN 以及扩展模块中的 Bloom 。默认情况下,CREATE INDEX 命令创建的是 B-tree 索引,它适用于绝大多数常见场景。
B-Tree 索引
B-tree(多路平衡树)是 PostgreSQL 的默认索引类型。任何可以被排序为明确定义的线性顺序的数据类型都可以使用 B-tree 索引。
B-tree 索引支持以下运算符的查询条件:
- 等值比较:
= - 范围比较:
<、<=、>、>= - 组合条件:
BETWEEN、IN IS NULL与IS NOT NULL- 前缀匹配的模式查询:
LIKE 'foo%'或~ '^foo'(需注意 locale 设置)
B-tree 索引还可以用于返回已排序的数据,在某些场景下可避免额外的显式排序操作。
PostgreSQL B-tree 索引是一种多级树结构 ,树的每一层均可作为页面的双向链表。索引的第一个段文件起始位置存储着一个固定的元页面(Metapage) 。树的最底层为叶子页面(Leaf Pages) ,存储指向表行的指针(即 CTID);其余层级为内部页面(Internal Pages),存储指向下一层级的指针。通常情况下,超过 99% 的页面为叶子页面。
Hash 索引
Hash 索引存储的是索引列值的 32 位哈希码,因此仅支持等值比较(= 运算符)。它不适用于范围查询或排序操作。
GiST、SP-GiST、GIN、BRIN
这些索引类型分别适用于不同的特殊场景:
- GiST(Generalized Search Tree):一种通用的索引基础设施,支持几何数据类型、全文搜索、最近邻搜索等。
- SP-GiST(Space-Partitioned GiST):适用于非平衡数据结构,如四叉树、kd-tree 等。
- GIN(Generalized Inverted Index):通用倒排索引,适用于数组、全文搜索等包含多个键值的数据类型。
- BRIN(Block Range Index):块范围索引,适用于数据物理分布与值域高度相关的超大表。
常见索引类型详解
主键索引(Primary Key Index)
主键约束会自动在对应列上创建一个唯一索引。主键索引具有以下特性:
- 值必须唯一(不可重复)
- 值不可为空 (
NOT NULL)
sql
CREATE TABLE test_pk (
id INT PRIMARY KEY
);
-- 查看表上的索引
\d test_pk
-- 输出中可见 "test_pk_pkey" PRIMARY KEY, btree (id)
尝试插入重复值或空值将触发错误:
sql
INSERT INTO test_pk VALUES (1); -- 成功
INSERT INTO test_pk VALUES (1); -- ERROR: duplicate key value violates unique constraint
INSERT INTO test_pk VALUES (NULL); -- ERROR: null value in column "id" violates not-null constraint
唯一索引(Unique Index)
唯一索引与主键索引类似,要求列值唯一,但允许为空值(NULL):
sql
CREATE TABLE test_unique (
id INT
);
CREATE UNIQUE INDEX idx_unique_id ON test_unique (id);
INSERT INTO test_unique VALUES (1); -- 成功
INSERT INTO test_unique VALUES (1); -- ERROR: duplicate key value violates unique constraint
INSERT INTO test_unique VALUES (NULL); -- 成功
INSERT INTO test_unique VALUES (NULL); -- 成功(NULL 不等于 NULL)
需要注意的是,在 SQL 标准中,NULL 不等于任何值(包括 NULL 本身),因此唯一索引允许存在多个 NULL 值。
表达式索引(Expression Index / Functional Index)
表达式索引(亦称函数索引)的索引列不一定是表的原始列,而是基于一个或多个列计算的函数或标量表达式。当查询条件中使用了相同的表达式时,优化器便可利用该索引。
适用场景 :查询中经常对列应用函数(如 LOWER、UPPER、日期截断等),且这些函数为不可变(IMMUTABLE) 的。
sql
CREATE TABLE test_expr (
info TEXT
);
-- 常规索引无法支持大小写不敏感的查询
CREATE INDEX idx_info ON test_expr (info);
-- 查询无法使用 idx_info(因为条件中使用了函数)
SELECT * FROM test_expr WHERE LOWER(info) = 'hello';
-- 创建表达式索引
CREATE INDEX idx_info_lower ON test_expr (LOWER(info));
-- 此时查询可以使用 idx_info_lower
SELECT * FROM test_expr WHERE LOWER(info) = 'hello';
表达式索引的语法要求:当表达式仅为函数调用时,括号可省略;否则需要用括号将表达式括起。
sql
-- 函数调用:括号可省略
CREATE INDEX idx1 ON test_expr (LOWER(info));
-- 一般表达式:必须加括号
CREATE INDEX idx2 ON test_expr ((info || 'suffix'));
表达式索引的维护成本相对较高,因为每次插入或更新(非 HOT 更新)时都需要重新计算表达式值。但在查询时,表达式值已存储在索引中,无需重新计算。
多列索引(Multicolumn Index)
多列索引是在表的多个列上定义的索引。当查询条件同时涉及这些列时,多列索引可以一次性过滤多个条件,避免多次索引扫描或回表操作。
sql
CREATE TABLE test_multi (
id INT,
info TEXT
);
-- 创建多列索引(最多支持 32 列)
CREATE INDEX idx_multi ON test_multi (id, info);
-- 以下查询可以利用该索引
SELECT * FROM test_multi WHERE id = 100 AND info = 'xxx';
SELECT * FROM test_multi WHERE id = 100; -- 也可利用(最左前缀原则)
-- 以下查询无法利用该索引(未使用索引的最左列)
SELECT * FROM test_multi WHERE info = 'xxx';
多列索引遵循最左前缀匹配 原则:查询条件必须包含索引定义中最左侧的列,才能使用该索引。在设计多列索引时,应将选择性较高(即唯一值较多)的列放在前面。
部分索引(Partial Index)
部分索引(Partial Index)是在表的子集 上建立的索引,该子集由一个条件表达式(称为谓词 Predicate )定义。索引中仅包含满足该谓词的行。
适用场景:
- 排除常见值:如果某个值出现在大多数行中,查询优化器通常不会使用索引(因为选择性太低),将这些行排除在索引之外可以减小索引体积。
- 仅索引热点数据:如果业务查询只关注表中的一小部分数据(如"未结订单"),可仅对该子集建立索引。
sql
CREATE TABLE test_partial (
id INT
);
-- 插入100万行数据(假设大部分查询只关注 id < 50000 的行)
INSERT INTO test_partial (id) SELECT generate_series(1, 1000000);
-- 创建部分索引:仅索引 id < 50000 的行
CREATE INDEX idx_partial ON test_partial (id) WHERE id < 50000;
-- 查询可以使用该部分索引
SELECT * FROM test_partial WHERE id = 100;
-- 查询无法使用该部分索引(id 不在索引的谓词范围内)
SELECT * FROM test_partial WHERE id = 60000;
部分索引的优势在于节省存储空间 和降低维护成本------索引更新操作仅需在满足谓词条件时进行。
索引的创建与删除
创建索引
CREATE INDEX 命令的基本语法如下:
sql
CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] name ]
ON table_name [ USING method ]
( { column_name | ( expression ) } [ ASC | DESC ] [, ...] )
[ WHERE predicate ];
各参数说明:
UNIQUE:创建唯一索引,系统会在索引创建时及每次数据变更时检查重复值。CONCURRENTLY:在线创建索引,不阻塞表的并发写入操作。IF NOT EXISTS:若同名索引已存在,则不抛出错误。USING method:指定索引方法,默认为btree。WHERE predicate:创建部分索引的谓词条件。
命名规则 :若未显式指定索引名称,PostgreSQL 会自动生成一个名称,规则为 {表名}_{列名}_idx(或类似模式)。需要注意的是,PostgreSQL 中的标识符(包括索引名)最大长度为 63 个字符,超长名称会被自动截断。
删除索引
sql
DROP INDEX index_name;
由于 PostgreSQL 中索引名在同一 Schema 下是唯一的,因此无需指定表名。
索引的维护成本与创建时机
索引的两面性
索引虽然能大幅提升查询性能,但并非"多多益善"。索引的维护成本体现在以下方面:
- 存储空间:索引本身需要占用额外的磁盘空间。
- 写入性能损耗 :每次
INSERT、UPDATE、DELETE操作都需要同步更新索引。更新操作即使只修改了未被索引的列,也可能需要更新索引中的物理指针(指向新行版本)。 - MVCC 版本链:在 PostgreSQL 的 MVCC 机制下,每次更新都会产生新的行版本,索引也需要为每个新版本维护对应的索引项,这可能导致"版本抖动(Version Churn)"。
何时应该创建索引
以下场景适合创建索引:
- 高选择性列:列中唯一值较多(如身份证号、邮箱、ID 等),索引能够有效过滤大部分数据。
- **频繁出现在
WHERE、JOIN、ORDER BY、GROUP BY子句中的列。 - 稀疏访问模式 :业务查询仅关注表中的部分数据,适合使用部分索引。
以下场景不适合创建索引:
- 低选择性列:列中唯一值极少(如性别,只有"男"/"女"),索引无法有效过滤数据,反而增加了维护开销。
- 频繁更新的表:索引的维护成本可能超过查询收益。
- 小表:顺序扫描的开销本身很低,索引带来的收益微乎其微。
在线创建索引:CREATE INDEX CONCURRENTLY
默认情况下,CREATE INDEX 会持有表上的排他锁(Access Exclusive Lock) ,阻塞所有写入操作(INSERT、UPDATE、DELETE),直到索引创建完成。对于生产环境中的大表,这可能导致长时间的写入阻塞。
PostgreSQL 提供了 CONCURRENTLY 选项来在线创建索引:
sql
CREATE INDEX CONCURRENTLY idx_name ON table_name (column);
使用 CONCURRENTLY 时,PostgreSQL 不会阻塞表的并发写入操作。但需要注意以下限制:
- 创建速度较慢:需要执行两次表扫描,并等待所有可能修改或使用该索引的事务结束。
- 创建失败时可能留下无效索引,需要手动清理。
- 不能在事务块内执行。
API 速览
本节梳理了本文涉及的核心 PostgreSQL SQL 命令与系统功能。
CREATE INDEX
| 属性 | 说明 |
|---|---|
| 所属 | PostgreSQL DDL 命令 |
| 语法 | `CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] name ] ON table_name [ USING method ] ( { column_name |
| 参数 | UNIQUE:唯一索引;CONCURRENTLY:在线创建;IF NOT EXISTS:条件创建;USING method:索引方法(默认 btree);WHERE predicate:部分索引谓词 |
| 返回值 | 无(DDL 命令) |
| 适用版本 | PostgreSQL 8.0+(CONCURRENTLY 自 8.2 引入,持续增强) |
示例:
sql
-- 基本 B-Tree 索引
CREATE INDEX idx_employees_last_name ON employees (last_name);
-- 唯一索引
CREATE UNIQUE INDEX idx_employees_email ON employees (email);
-- 表达式索引
CREATE INDEX idx_employees_lower_email ON employees (LOWER(email));
-- 多列索引
CREATE INDEX idx_employees_name_dept ON employees (last_name, department_id);
-- 部分索引
CREATE INDEX idx_employees_active ON employees (last_name) WHERE status = 'ACTIVE';
-- 在线创建索引
CREATE INDEX CONCURRENTLY idx_employees_hire_date ON employees (hire_date);
DROP INDEX
| 属性 | 说明 |
|---|---|
| 所属 | PostgreSQL DDL 命令 |
| 语法 | `DROP INDEX [ CONCURRENTLY ] [ IF EXISTS ] name [, ...] [ CASCADE |
| 参数 | CONCURRENTLY:在线删除;IF EXISTS:条件删除;CASCADE:级联删除依赖对象 |
| 返回值 | 无(DDL 命令) |
| 适用版本 | PostgreSQL 8.0+ |
示例:
sql
DROP INDEX idx_employees_last_name;
DROP INDEX CONCURRENTLY IF EXISTS idx_employees_hire_date;
\d --- 查看表结构(psql 元命令)
| 属性 | 说明 |
|---|---|
| 所属 | psql 客户端元命令 |
| 语法 | \d table_name |
| 功能 | 显示指定表的列信息、索引、约束等 |
示例:
sql
\d test_index
-- 输出示例:
-- Table "public.test_index"
-- Column | Type | Collation | Nullable | Default
-- --------+---------+-----------+----------+---------
-- id | integer | | |
-- Indexes:
-- "idx_test_index_id" btree (id)
系统列 CTID
| 属性 | 说明 |
|---|---|
| 所属 | PostgreSQL 系统列(隐式) |
| 类型 | tid(Tuple ID) |
| 内容 | 数据块编号(Block Number)+ 块内偏移量(Offset) |
| 用途 | 索引中存储的物理行指针,用于精确定位数据行 |
示例:
sql
SELECT ctid, * FROM test_index WHERE id = 99999;
-- ctid 示例输出:(100, 5) 表示第100个数据块的第5个偏移位置
Demo 简单示例
以下是一个完整的 Node.js 示例,演示了 PostgreSQL 索引的创建、查询性能对比以及各类索引的使用。
运行说明
环境要求:
- Node.js 16+
- PostgreSQL 12+
- 已安装
pg包
安装依赖:
bash
npm init -y
npm install pg
启动方式:
bash
node index.js
完整代码
js
const { Client } = require('pg');
// PostgreSQL 连接配置
const client = new Client({
host: 'localhost',
port: 5432,
database: 'testdb',
user: 'postgres',
password: 'postgres',
});
async function runDemo() {
await client.connect();
console.log('Connected to PostgreSQL');
try {
// ============================================================
// 1. 创建测试表并插入数据
// ============================================================
await client.query(`
DROP TABLE IF EXISTS demo_index CASCADE;
CREATE TABLE demo_index (
id INT,
info TEXT,
status VARCHAR(20)
);
`);
console.log('Table "demo_index" created.');
// 插入 50 万行测试数据
await client.query(`
INSERT INTO demo_index (id, info, status)
SELECT
generate_series(1, 500000),
'info_' || generate_series(1, 500000),
CASE WHEN generate_series(1, 500000) % 10 = 0 THEN 'INACTIVE' ELSE 'ACTIVE' END
`);
console.log('Inserted 500,000 rows.');
// ============================================================
// 2. 无索引时的顺序扫描(Sequential Scan)
// ============================================================
const beforeNoIndex = Date.now();
const res1 = await client.query(
'SELECT * FROM demo_index WHERE id = 123456;'
);
const afterNoIndex = Date.now();
console.log(`[No Index] Query time: ${afterNoIndex - beforeNoIndex} ms`);
// ============================================================
// 3. 创建 B-Tree 索引
// ============================================================
// PostgreSQL 原生指令:
// CREATE INDEX idx_demo_index_id ON demo_index (id);
await client.query('CREATE INDEX idx_demo_index_id ON demo_index (id);');
console.log('Index "idx_demo_index_id" created.');
const beforeWithIndex = Date.now();
const res2 = await client.query(
'SELECT * FROM demo_index WHERE id = 123456;'
);
const afterWithIndex = Date.now();
console.log(`[With Index] Query time: ${afterWithIndex - beforeWithIndex} ms`);
// ============================================================
// 4. 表达式索引(Expression Index)
// ============================================================
// PostgreSQL 原生指令:
// CREATE INDEX idx_demo_index_lower_info ON demo_index (LOWER(info));
await client.query(`
CREATE INDEX idx_demo_index_lower_info ON demo_index (LOWER(info));
`);
console.log('Expression index "idx_demo_index_lower_info" created.');
const beforeExpr = Date.now();
const res3 = await client.query(
"SELECT * FROM demo_index WHERE LOWER(info) = 'info_123456';"
);
const afterExpr = Date.now();
console.log(`[Expression Index] Query time: ${afterExpr - beforeExpr} ms`);
// ============================================================
// 5. 部分索引(Partial Index)
// ============================================================
// PostgreSQL 原生指令:
// CREATE INDEX idx_demo_index_active ON demo_index (id) WHERE status = 'ACTIVE';
await client.query(`
CREATE INDEX idx_demo_index_active ON demo_index (id)
WHERE status = 'ACTIVE';
`);
console.log('Partial index "idx_demo_index_active" created.');
// 查询可以使用部分索引(status = 'ACTIVE' 且 id 在索引范围内)
const beforePartial = Date.now();
const res4 = await client.query(
"SELECT * FROM demo_index WHERE id = 123456 AND status = 'ACTIVE';"
);
const afterPartial = Date.now();
console.log(`[Partial Index - ACTIVE] Query time: ${afterPartial - beforePartial} ms`);
// 查询无法使用部分索引(status = 'INACTIVE' 不在索引谓词范围内)
const beforePartialMiss = Date.now();
const res5 = await client.query(
"SELECT * FROM demo_index WHERE id = 123456 AND status = 'INACTIVE';"
);
const afterPartialMiss = Date.now();
console.log(`[Partial Index - INACTIVE] Query time: ${afterPartialMiss - beforePartialMiss} ms`);
// ============================================================
// 6. 查看索引信息
// ============================================================
// PostgreSQL 原生指令:\d demo_index
const indexInfo = await client.query(`
SELECT
indexname,
indexdef
FROM pg_indexes
WHERE tablename = 'demo_index';
`);
console.log('\n=== Indexes on "demo_index" ===');
indexInfo.rows.forEach(row => {
console.log(` ${row.indexname}: ${row.indexdef}`);
});
// ============================================================
// 7. 查看查询计划(EXPLAIN)
// ============================================================
const explainResult = await client.query(`
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM demo_index WHERE id = 123456;
`);
console.log('\n=== EXPLAIN (with index) ===');
console.log(explainResult.rows.map(r => r['QUERY PLAN']).join('\n'));
} catch (err) {
console.error('Error:', err.message);
} finally {
await client.end();
}
}
runDemo();
代码说明
- 连接与建表 :使用
pg驱动连接 PostgreSQL,创建demo_index表并插入 50 万行测试数据。 - 性能对比 :分别在无索引和有索引的情况下执行相同的
WHERE id = ?查询,对比执行时间。 - 表达式索引 :在
LOWER(info)上创建索引,演示函数索引的用法。 - 部分索引 :仅对
status = 'ACTIVE'的行创建索引,演示部分索引的过滤效果。 - 索引信息查询 :通过
pg_indexes系统视图查看表上的所有索引定义。 - 执行计划分析 :使用
EXPLAIN (ANALYZE, BUFFERS)查看索引扫描的详细执行计划。
技术点总结
- 顺序扫描 vs. 索引扫描:通过实际计时数据直观对比两种扫描方式的性能差异。
- B-Tree 索引:PostgreSQL 默认索引类型,支持等值和范围查询。
- 表达式索引:在函数或表达式结果上建立索引,支持大小写不敏感查询等场景。
- 部分索引:仅在表子集上建立索引,节省空间并降低维护成本。
- 执行计划分析 :使用
EXPLAIN验证索引是否被使用。
多语言示例程序扩展
本文基于上面的 Node.js 示例,扩展提供 Go 、Python 和 Java 三种语言的完整实现。所有示例均演示了 PostgreSQL 索引的创建、查询性能对比以及表达式索引、部分索引的使用方式。
通用环境要求
- PostgreSQL 12+,已创建测试数据库(如
testdb) - 各语言对应的数据库驱动(详见各节)
- 表结构统一为:
sql
CREATE TABLE demo_index (
id INT,
info TEXT,
status VARCHAR(20)
);
插入 50 万行测试数据:
sql
INSERT INTO demo_index (id, info, status)
SELECT
generate_series(1, 500000),
'info_' || generate_series(1, 500000),
CASE WHEN generate_series(1, 500000) % 10 = 0 THEN 'INACTIVE' ELSE 'ACTIVE' END;
索引创建语句(按顺序执行):
sql
CREATE INDEX idx_demo_index_id ON demo_index (id);
CREATE INDEX idx_demo_index_lower_info ON demo_index (LOWER(info));
CREATE INDEX idx_demo_index_active ON demo_index (id) WHERE status = 'ACTIVE';
Go 示例程序
运行说明
- 环境 :Go 1.18+,安装驱动:
go get github.com/lib/pq - 启动 :
go run main.go - 修改连接字符串中的数据库凭据。
完整代码
go
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/lib/pq"
)
func main() {
// 连接字符串:host=localhost port=5432 user=postgres password=postgres dbname=testdb sslmode=disable
connStr := "postgres://postgres:postgres@localhost:5432/testdb?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatal(err)
}
fmt.Println("Connected to PostgreSQL")
// 1. 删除并重建表(若需要)
_, err = db.Exec(`DROP TABLE IF EXISTS demo_index CASCADE`)
if err != nil {
log.Fatal(err)
}
_, err = db.Exec(`CREATE TABLE demo_index (
id INT,
info TEXT,
status VARCHAR(20)
)`)
if err != nil {
log.Fatal(err)
}
fmt.Println("Table created")
// 插入 50 万行
_, err = db.Exec(`
INSERT INTO demo_index (id, info, status)
SELECT
generate_series(1, 500000),
'info_' || generate_series(1, 500000),
CASE WHEN generate_series(1, 500000) % 10 = 0 THEN 'INACTIVE' ELSE 'ACTIVE' END
`)
if err != nil {
log.Fatal(err)
}
fmt.Println("Inserted 500,000 rows")
// 2. 无索引查询
start := time.Now()
row := db.QueryRow(`SELECT * FROM demo_index WHERE id = 123456`)
var id int
var info string
var status string
if err := row.Scan(&id, &info, &status); err != nil {
log.Fatal(err)
}
elapsed := time.Since(start)
fmt.Printf("[No Index] Query time: %v ms\n", elapsed.Milliseconds())
// 3. 创建索引 (id)
_, err = db.Exec(`CREATE INDEX idx_demo_index_id ON demo_index (id)`)
if err != nil {
log.Fatal(err)
}
fmt.Println("Index idx_demo_index_id created")
start = time.Now()
row = db.QueryRow(`SELECT * FROM demo_index WHERE id = 123456`)
if err := row.Scan(&id, &info, &status); err != nil {
log.Fatal(err)
}
elapsed = time.Since(start)
fmt.Printf("[With Index] Query time: %v ms\n", elapsed.Milliseconds())
// 4. 表达式索引
_, err = db.Exec(`CREATE INDEX idx_demo_index_lower_info ON demo_index (LOWER(info))`)
if err != nil {
log.Fatal(err)
}
fmt.Println("Expression index idx_demo_index_lower_info created")
start = time.Now()
row = db.QueryRow(`SELECT * FROM demo_index WHERE LOWER(info) = 'info_123456'`)
if err := row.Scan(&id, &info, &status); err != nil {
log.Fatal(err)
}
elapsed = time.Since(start)
fmt.Printf("[Expression Index] Query time: %v ms\n", elapsed.Milliseconds())
// 5. 部分索引
_, err = db.Exec(`CREATE INDEX idx_demo_index_active ON demo_index (id) WHERE status = 'ACTIVE'`)
if err != nil {
log.Fatal(err)
}
fmt.Println("Partial index idx_demo_index_active created")
start = time.Now()
row = db.QueryRow(`SELECT * FROM demo_index WHERE id = 123456 AND status = 'ACTIVE'`)
if err := row.Scan(&id, &info, &status); err != nil {
log.Fatal(err)
}
elapsed = time.Since(start)
fmt.Printf("[Partial Index - ACTIVE] Query time: %v ms\n", elapsed.Milliseconds())
// 查询无法使用部分索引 (INACTIVE)
start = time.Now()
row = db.QueryRow(`SELECT * FROM demo_index WHERE id = 123456 AND status = 'INACTIVE'`)
if err := row.Scan(&id, &info, &status); err != nil {
// 可能无数据,忽略
}
elapsed = time.Since(start)
fmt.Printf("[Partial Index - INACTIVE] Query time: %v ms\n", elapsed.Milliseconds())
// 6. 查看索引定义
rows, err := db.Query(`
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'demo_index'
`)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
fmt.Println("\n=== Indexes on 'demo_index' ===")
for rows.Next() {
var idxName, idxDef string
if err := rows.Scan(&idxName, &idxDef); err != nil {
log.Fatal(err)
}
fmt.Printf(" %s: %s\n", idxName, idxDef)
}
// 7. EXPLAIN 分析
var plan string
err = db.QueryRow(`
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM demo_index WHERE id = 123456
`).Scan(&plan)
if err != nil {
log.Fatal(err)
}
fmt.Println("\n=== EXPLAIN (with index) ===")
fmt.Println(plan)
}
技术点总结
- 使用
database/sql+lib/pq驱动操作 PostgreSQL。 - 时序测量采用
time.Since。 - 执行
EXPLAIN并获取文本结果。 - 错误处理遵循 Go 惯例。
Python 示例程序
运行说明
- 环境 :Python 3.8+,安装依赖:
pip install psycopg2-binary - 启动 :
python demo.py - 修改连接参数(
host,port,database,user,password)。
完整代码
python
import psycopg2
import time
def main():
conn = psycopg2.connect(
host="localhost",
port=5432,
database="testdb",
user="postgres",
password="postgres"
)
conn.autocommit = True
cur = conn.cursor()
print("Connected to PostgreSQL")
# 1. 创建表
cur.execute("DROP TABLE IF EXISTS demo_index CASCADE")
cur.execute("""
CREATE TABLE demo_index (
id INT,
info TEXT,
status VARCHAR(20)
)
""")
print("Table created")
# 插入 50 万行
cur.execute("""
INSERT INTO demo_index (id, info, status)
SELECT
generate_series(1, 500000),
'info_' || generate_series(1, 500000),
CASE WHEN generate_series(1, 500000) % 10 = 0 THEN 'INACTIVE' ELSE 'ACTIVE' END
""")
print("Inserted 500,000 rows")
# 2. 无索引查询
start = time.time()
cur.execute("SELECT * FROM demo_index WHERE id = 123456")
row = cur.fetchone()
elapsed = (time.time() - start) * 1000
print(f"[No Index] Query time: {elapsed:.2f} ms")
# 3. 创建索引 (id)
cur.execute("CREATE INDEX idx_demo_index_id ON demo_index (id)")
print("Index idx_demo_index_id created")
start = time.time()
cur.execute("SELECT * FROM demo_index WHERE id = 123456")
row = cur.fetchone()
elapsed = (time.time() - start) * 1000
print(f"[With Index] Query time: {elapsed:.2f} ms")
# 4. 表达式索引
cur.execute("CREATE INDEX idx_demo_index_lower_info ON demo_index (LOWER(info))")
print("Expression index idx_demo_index_lower_info created")
start = time.time()
cur.execute("SELECT * FROM demo_index WHERE LOWER(info) = 'info_123456'")
row = cur.fetchone()
elapsed = (time.time() - start) * 1000
print(f"[Expression Index] Query time: {elapsed:.2f} ms")
# 5. 部分索引
cur.execute("CREATE INDEX idx_demo_index_active ON demo_index (id) WHERE status = 'ACTIVE'")
print("Partial index idx_demo_index_active created")
start = time.time()
cur.execute("SELECT * FROM demo_index WHERE id = 123456 AND status = 'ACTIVE'")
row = cur.fetchone()
elapsed = (time.time() - start) * 1000
print(f"[Partial Index - ACTIVE] Query time: {elapsed:.2f} ms")
start = time.time()
cur.execute("SELECT * FROM demo_index WHERE id = 123456 AND status = 'INACTIVE'")
row = cur.fetchone()
elapsed = (time.time() - start) * 1000
print(f"[Partial Index - INACTIVE] Query time: {elapsed:.2f} ms")
# 6. 查看索引定义
cur.execute("SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'demo_index'")
print("\n=== Indexes on 'demo_index' ===")
for idx_name, idx_def in cur.fetchall():
print(f" {idx_name}: {idx_def}")
# 7. EXPLAIN 分析
cur.execute("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT * FROM demo_index WHERE id = 123456")
plan = cur.fetchone()[0]
print("\n=== EXPLAIN (with index) ===")
print(plan)
cur.close()
conn.close()
if __name__ == "__main__":
main()
技术点总结
- 使用
psycopg2驱动,autocommit=True简化事务管理。 - 时间测量使用
time.time()转换为毫秒。 EXPLAIN结果以文本形式获取并打印。- 简洁的错误处理(实际生产需增强)。
Java 示例程序
运行说明
- 环境 :JDK 11+,Maven 或直接添加 PostgreSQL JDBC 驱动(
org.postgresql:postgresql:42.6.0) - 编译运行 :使用 Maven 或
javac -cp postgresql-42.6.0.jar Demo.java并java -cp .:postgresql-42.6.0.jar Demo(Windows 用;分隔) - 修改连接 URL 中的用户名/密码/数据库名。
完整代码
java
import java.sql.*;
import java.util.Properties;
public class Demo {
public static void main(String[] args) {
String url = "jdbc:postgresql://localhost:5432/testdb";
Properties props = new Properties();
props.setProperty("user", "postgres");
props.setProperty("password", "postgres");
try (Connection conn = DriverManager.getConnection(url, props)) {
System.out.println("Connected to PostgreSQL");
// 1. 创建表
try (Statement stmt = conn.createStatement()) {
stmt.execute("DROP TABLE IF EXISTS demo_index CASCADE");
stmt.execute("""
CREATE TABLE demo_index (
id INT,
info TEXT,
status VARCHAR(20)
)
""");
System.out.println("Table created");
// 插入 50 万行
stmt.execute("""
INSERT INTO demo_index (id, info, status)
SELECT
generate_series(1, 500000),
'info_' || generate_series(1, 500000),
CASE WHEN generate_series(1, 500000) % 10 = 0 THEN 'INACTIVE' ELSE 'ACTIVE' END
""");
System.out.println("Inserted 500,000 rows");
}
// 2. 无索引查询
long start = System.currentTimeMillis();
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM demo_index WHERE id = 123456")) {
if (rs.next()) {
// 读取数据(略)
}
}
long elapsed = System.currentTimeMillis() - start;
System.out.println("[No Index] Query time: " + elapsed + " ms");
// 3. 创建索引 (id)
try (Statement stmt = conn.createStatement()) {
stmt.execute("CREATE INDEX idx_demo_index_id ON demo_index (id)");
System.out.println("Index idx_demo_index_id created");
}
start = System.currentTimeMillis();
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM demo_index WHERE id = 123456")) {
if (rs.next()) {}
}
elapsed = System.currentTimeMillis() - start;
System.out.println("[With Index] Query time: " + elapsed + " ms");
// 4. 表达式索引
try (Statement stmt = conn.createStatement()) {
stmt.execute("CREATE INDEX idx_demo_index_lower_info ON demo_index (LOWER(info))");
System.out.println("Expression index idx_demo_index_lower_info created");
}
start = System.currentTimeMillis();
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM demo_index WHERE LOWER(info) = 'info_123456'")) {
if (rs.next()) {}
}
elapsed = System.currentTimeMillis() - start;
System.out.println("[Expression Index] Query time: " + elapsed + " ms");
// 5. 部分索引
try (Statement stmt = conn.createStatement()) {
stmt.execute("CREATE INDEX idx_demo_index_active ON demo_index (id) WHERE status = 'ACTIVE'");
System.out.println("Partial index idx_demo_index_active created");
}
start = System.currentTimeMillis();
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM demo_index WHERE id = 123456 AND status = 'ACTIVE'")) {
if (rs.next()) {}
}
elapsed = System.currentTimeMillis() - start;
System.out.println("[Partial Index - ACTIVE] Query time: " + elapsed + " ms");
start = System.currentTimeMillis();
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM demo_index WHERE id = 123456 AND status = 'INACTIVE'")) {
if (rs.next()) {}
}
elapsed = System.currentTimeMillis() - start;
System.out.println("[Partial Index - INACTIVE] Query time: " + elapsed + " ms");
// 6. 查看索引定义
System.out.println("\n=== Indexes on 'demo_index' ===");
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'demo_index'")) {
while (rs.next()) {
System.out.printf(" %s: %s%n", rs.getString(1), rs.getString(2));
}
}
// 7. EXPLAIN 分析
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT * FROM demo_index WHERE id = 123456")) {
System.out.println("\n=== EXPLAIN (with index) ===");
while (rs.next()) {
System.out.println(rs.getString(1));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
技术点总结
- 使用 JDBC 驱动,通过
DriverManager获取连接。 - 使用
try-with-resources自动释放资源。 - 时间测量使用
System.currentTimeMillis()。 - 执行
EXPLAIN并逐行打印结果。
多语言对比总结
| 语言 | 驱动/库 | 特点 |
|---|---|---|
| Go | lib/pq |
编译型,错误处理显式,性能优异 |
| Python | psycopg2 |
动态类型,代码简洁,适合快速原型 |
| Java | JDBC | 类型安全,资源管理规范,适合企业应用 |
以上三种语言实现均与 Node.js 示例逻辑一致,可直接运行并观察索引带来的性能提升。
项目难点与解决方案
核心难点
- 索引维护成本与查询收益的权衡:在生产环境中,不恰当的索引设计可能导致写入性能严重下降,而查询性能却未获得显著提升。
- 在线建索引的复杂性 :
CREATE INDEX CONCURRENTLY虽然不阻塞写入,但创建速度较慢、可能留下无效索引,且不能在事务块中执行。 - 标识符长度限制:PostgreSQL 对象名最大长度为 63 个字符,超长名称会被自动截断,可能导致不同对象生成相同的截断名称,引发命名冲突。
解决方案
- 索引设计原则:优先在高选择性列上创建索引;对于低选择性列(如性别、状态枚举),评估是否适合使用部分索引。
- 部分索引与表达式索引的合理运用:针对特定的查询模式,使用部分索引缩小索引范围,使用表达式索引支持函数查询。
- 在线建索引的规范流程 :在生产环境中使用
CREATE INDEX CONCURRENTLY时,需监控创建进度,并在失败后及时清理无效索引。 - 命名规范 :制定索引命名规范(如
idx_{表名}_{列名}),避免依赖自动命名,同时确保总长度不超过 63 个字符。
广度
本文覆盖了 PostgreSQL 索引的基本概念 、主要类型 (B-Tree、唯一索引、表达式索引、多列索引、部分索引)、创建与删除语法 、在线建索引 以及索引的选择性评估,为读者建立了完整的索引知识入门框架。
深度
深入探讨了 B-Tree 索引的底层数据结构 (多级树结构、元页面、叶子/内部页面)、MVCC 下的版本链与索引维护机制 以及部分索引与表达式索引的适用场景与限制。
复杂度
索引设计涉及存储空间 、写入性能 、查询性能 三者的平衡,需要结合业务查询模式、数据分布特征和系统负载进行综合评估。CREATE INDEX CONCURRENTLY 的引入进一步增加了索引管理的复杂度。
官方文档
- PostgreSQL CREATE INDEX 官方文档 :https://www.postgresql.org/docs/current/sql-createindex.html
- PostgreSQL 索引类型 :https://www.postgresql.org/docs/current/indexes-types.html
- PostgreSQL 表达式索引 :https://www.postgresql.org/docs/current/indexes-expressional.html
- PostgreSQL 部分索引 :https://www.postgresql.org/docs/current/indexes-partial.html
- PostgreSQL B-Tree 索引实现 :https://www.postgresql.org/docs/current/btree.html
- PostgreSQL B-Tree 内部实现细节 :https://www.postgresql.org/docs/current/btree-implementation.html
参考链接
- PostgreSQL Wiki - Indexes :https://wiki.postgresql.org/wiki/Indexes
- PostgreSQL 中文文档 - 索引 :https://docs.postgresql.tw/13/the-sql-language/indexes
- PostgreSQL 性能调优 - 索引最佳实践 :https://pganalyze.com/blog/postgresql-indexing
总结
本文系统性地介绍了 PostgreSQL 索引的核心概念、主要类型及其适用场景。索引通过 B-Tree 等多级树结构将查询复杂度从 O ( n ) O(n) O(n) 降低至 O ( log n ) O(\log n) O(logn),但同时也带来了存储空间和写入维护的额外开销。
在实际生产中,索引设计需遵循"高选择性优先"原则,并结合部分索引、表达式索引等高级特性进行针对性优化。CREATE INDEX CONCURRENTLY 提供了在线建索引的能力,但需谨慎处理其性能与失败恢复问题。
索引的底层实现(如 B-Tree 的页分裂、MVCC 版本链下的索引维护)是深入理解索引行为的关键。后续文章将深入探讨索引的内部原理、扫描方式及查询优化器的索引选择策略。