强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

07 - 索引详解

第 07 章 · 索引详解

索引是数据库性能优化的核心手段。PostgreSQL 提供了最丰富的索引类型,以适应不同的数据和查询模式。


7.1 索引概览

索引类型数据结构适用场景PG 版本
B-TreeB+ 树等值、范围、排序所有版本
Hash哈希表纯等值查询PG 10+(WAL 安全)
GiST平衡树几何、全文搜索、Range所有版本
GIN倒排索引全文搜索、数组、JSONB所有版本
BRIN块范围摘要大表顺序数据PG 9.5+
Bloom布隆过滤器多列等值组合PG 9.6+

7.2 B-Tree 索引

B-Tree 是默认索引类型,适用于绝大多数场景。

-- 创建 B-Tree 索引(默认类型)
CREATE INDEX idx_emp_email ON employees (email);
-- 等价于
CREATE INDEX idx_emp_email ON employees USING BTREE (email);

-- 多列索引
CREATE INDEX idx_emp_dept_salary ON employees (department, salary);

-- 倒序索引
CREATE INDEX idx_emp_salary_desc ON employees (salary DESC);

-- 表达式索引
CREATE INDEX idx_emp_lower_email ON employees (LOWER(email));

-- 部分索引(带 WHERE 条件)
CREATE INDEX idx_emp_active ON employees (email) WHERE is_active = TRUE;

-- 覆盖索引(INCLUDE,PG 11+)
CREATE INDEX idx_emp_covering ON employees (department) INCLUDE (name, salary);
-- 查询 SELECT name, salary FROM employees WHERE department = '工程部' 可以只用索引

B-Tree 适用操作

操作符说明
=, <>等值/不等
<, <=, >, >=范围比较
BETWEEN范围
IN多值等值
IS NULL / IS NOT NULL空值判断
LIKE 'abc%'前缀匹配(可使用索引)
LIKE '%abc'后缀匹配(无法使用索引)
ORDER BY ... LIMIT排序+分页

⚠️ 注意事项:多列索引遵循最左前缀原则。索引 (a, b, c) 可以用于 WHERE a = 1WHERE a = 1 AND b = 2WHERE a = 1 AND b = 2 AND c = 3,但不能用于 WHERE b = 2


7.3 Hash 索引

Hash 索引仅支持等值查询,但比 B-Tree 更小、更快。

CREATE INDEX idx_emp_name_hash ON employees USING HASH (name);

-- 仅适用于等值查询
SELECT * FROM employees WHERE name = 'Alice';  -- ✅ 使用 Hash 索引
SELECT * FROM employees WHERE name > 'Alice';  -- ❌ 不使用 Hash 索引
特性B-TreeHash
等值查询✅(更快)
范围查询
排序
索引大小较大较小
WAL 安全✅(PG 10+)

⚠️ 注意事项:PG 10 之前 Hash 索引不支持 WAL,崩溃后需要重建。PG 10+ 已解决此问题。但 Hash 索引的实际使用场景较少,通常 B-Tree 已足够。


7.4 GiST 索引

GiST(Generalized Search Tree)支持多种数据类型的复杂查询。

-- 需要 btree_gist 扩展(排他约束用)
CREATE EXTENSION IF NOT EXISTS btree_gist;

-- 几何数据索引
CREATE INDEX idx_geo ON places USING GIST (location);

-- Range 类型索引
CREATE INDEX idx_res_during ON reservations USING GIST (during);

-- 全文搜索索引
CREATE INDEX idx_fts ON articles USING GIST (to_tsvector('english', content));

-- 排他约束
CREATE TABLE rooms (
    id SERIAL PRIMARY KEY,
    room_no INT,
    during TSTZRANGE,
    EXCLUDE USING GIST (room_no WITH =, during WITH &&)
);

适用操作符:<<, >>, @>, <@, &&, ~=, <-> 等。


7.5 GIN 索引

GIN(Generalized Inverted Index)倒排索引,是 PostgreSQL 处理全文搜索、数组、JSONB 的核心。

-- JSONB GIN 索引
CREATE INDEX idx_events_data ON events USING GIN (data);

-- 数组 GIN 索引
CREATE INDEX idx_products_tags ON products USING GIN (tags);

-- 全文搜索 GIN 索引
CREATE INDEX idx_articles_fts ON articles USING GIN (
    to_tsvector('simple', title || ' ' || content)
);

-- jsonb_path_ops(仅支持 @> 操作符,但更小更快)
CREATE INDEX idx_events_data_path ON events USING GIN (data jsonb_path_ops);
GIN 索引类型支持的操作符说明
默认 GIN@>, <@, ?, `?, ?&, &&`
jsonb_path_ops@>仅包含查询,但体积更小

💡 技巧:如果 JSONB 查询只用 @>(包含查询),使用 jsonb_path_ops 可以大幅减小索引体积,构建速度也更快。


7.6 BRIN 索引

BRIN(Block Range Index)为超大表设计,索引极小。

-- BRIN 索引
CREATE INDEX idx_logs_created ON access_logs USING BRIN (created_at)
    WITH (pages_per_range = 128);

-- 适合场景:时序数据、自增 ID、时间戳
-- 数据特征:物理顺序与逻辑顺序高度相关
特性B-TreeBRIN
索引大小大(每行一个条目)极小(每个页范围一个摘要)
查询精度精确近似(需回表验证)
适用表大小任意超大表(百万行以上)
维护需要 VACUUM需要 VACUUM 更新摘要
适用数据任意物理有序数据
-- 比较索引大小
SELECT
    indexname,
    pg_size_pretty(pg_relation_size(indexname::regclass)) AS index_size
FROM pg_indexes
WHERE tablename = 'access_logs';

7.7 部分索引(Partial Index)

部分索引只索引满足特定条件的行,减少索引大小。

-- 只索引活跃用户
CREATE INDEX idx_active_users ON users (email) WHERE is_active = TRUE;

-- 只索引未完成订单
CREATE INDEX idx_pending_orders ON orders (created_at)
WHERE status IN (0, 1, 2);

-- 使用部分索引的查询必须包含索引条件
SELECT * FROM users WHERE email = '[email protected]' AND is_active = TRUE;  -- ✅
SELECT * FROM users WHERE email = '[email protected]';  -- ❌ 可能不使用部分索引

💡 技巧:部分索引在以下场景特别有用:

  • 状态机中只有少量"活跃"记录需要查询
  • 软删除表中大部分记录已删除
  • 需要为 NULL 值单独建立索引

7.8 索引管理

-- 查看表的所有索引
\d+ employees

-- 查看所有索引及其大小
SELECT
    schemaname, tablename, indexname,
    pg_size_pretty(pg_relation_size(indexname::regclass)) AS size,
    idx_scan AS scans
FROM pg_indexes
JOIN pg_stat_user_indexes USING (schemaname, tablename, indexname)
WHERE schemaname = 'public'
ORDER BY pg_relation_size(indexname::regclass) DESC;

-- 重建索引(不阻塞查询)
REINDEX INDEX CONCURRENTLY idx_emp_email;

-- 重建表的所有索引
REINDEX TABLE CONCURRENTLY employees;

-- 删除索引
DROP INDEX IF EXISTS idx_emp_email;

-- 创建索引时不阻塞写入(必须!)
CREATE INDEX CONCURRENTLY idx_new ON employees (department);

⚠️ 注意事项:生产环境创建索引务必使用 CONCURRENTLY。普通 CREATE INDEX 会锁表,阻塞所有写操作。CREATE INDEX CONCURRENTLY 耗时更长但不阻塞。


7.9 无用索引检测

-- 查找从未使用过的索引
SELECT
    schemaname, relname AS table, indexrelname AS index,
    pg_size_pretty(pg_relation_size(i.indexrelid)) AS size,
    idx_scan AS scans
FROM pg_stat_user_indexes i
JOIN pg_index USING (indexrelid)
WHERE idx_scan = 0
  AND NOT indisunique       -- 排除唯一索引
  AND NOT indisprimary      -- 排除主键
ORDER BY pg_relation_size(i.indexrelid) DESC;

-- 查找重复/冗余索引
SELECT
    a.indexrelid::regclass AS index_a,
    b.indexrelid::regclass AS index_b,
    pg_size_pretty(pg_relation_size(a.indexrelid)) AS size_a
FROM pg_index a
JOIN pg_index b ON a.indrelid = b.indrelid
    AND a.indexrelid <> b.indexrelid
    AND a.indkey::TEXT LIKE b.indkey::TEXT || '%'
WHERE a.indrelid::regclass::TEXT NOT LIKE 'pg_%';

7.10 索引策略最佳实践

索引设计 Checklist

检查项说明
☐ WHERE 条件列高频查询的过滤列
☐ JOIN 列外键/关联列
☐ ORDER BY 列排序列
☐ 覆盖索引INCLUDE 常用列,避免回表
☐ 部分索引只索引需要的部分
☐ 表达式索引函数/计算列
☐ 索引数量每个表不超过 5-6 个
☐ 索引大小监控索引膨胀

常见索引误区

误区正确做法
每列都建索引只在高频查询的列建索引
CREATE INDEX 不用 CONCURRENTLY生产必须用 CONCURRENTLY
不关注索引膨胀定期 REINDEX 或使用 pg_repack
LIKE ‘%xxx’ 建 B-Tree前缀模糊用 pg_trgm GIN
小表建索引小于 1 万行的表通常不需要索引

业务场景

场景推荐索引
用户登录查询B-Tree(email 唯一索引)
商品模糊搜索GIN(pg_trgm)
JSONB 条件查询GIN(data)或 GIN(data jsonb_path_ops)
时序数据查询BRIN(created_at)或 B-Tree
地理位置查询GiST(PostGIS geometry)
全文搜索GIN(tsvector)
标签过滤GIN(tags 数组)

扩展阅读