3. 复合索引设计:最左前缀原则、索引列顺序对查询性能的影响、冗余索引的识别与清理
复合索引(多列索引)是 PostgreSQL 中应对复杂查询条件最有力的工具,但设计不当反而会拖慢写入、浪费存储。本章从最左前缀原则出发,深入分析索引列顺序对查询性能的影响,并给出冗余索引的识别与清理方法。
3.1 最左前缀原则
PostgreSQL 的 B-Tree 复合索引按照索引定义中列的顺序构建一棵排序树。查询条件只有从索引的最左列开始匹配,才能有效利用该索引,这就是最左前缀原则。
核心规则:复合索引 (a, b, c) 可以加速的条件组合包括:
WHERE a = ?WHERE a = ? AND b = ?WHERE a = ? AND b = ? AND c = ?WHERE a = ? AND b > ? AND c = ?(b 上的范围查询会阻断后续列的等值匹配)
WHERE b = ?(跳过了最左列 a)WHERE c = ?(跳过了 a 和 b)
实践建议:在创建复合索引前,先列出业务中高频出现的 WHERE 条件组合,确保最左列是查询频率最高、选择性最好的列。
3.2 索引列顺序对查询性能的影响
复合索引中列的顺序不仅决定哪些查询能走索引,还直接影响索引的过滤效率和排序能力。
3.2.1 等值条件优先原则
将等值条件(=)的列放在范围条件(>、<、BETWEEN)之前,可以最大化索引的过滤粒度。
| 索引定义 | 查询条件 | 索引使用情况 | 性能说明 |
|---|---|---|---|
(status, created_at) |
WHERE status = 'active' AND created_at > '2024-01-01' |
完全匹配 | 先过滤 status,再范围扫描 created_at,高效 |
(created_at, status) |
WHERE status = 'active' AND created_at > '2024-01-01' |
部分匹配 | created_at 范围扫描后,status 无法利用索引过滤,需回表 |
(user_id, order_date) |
WHERE user_id = 123 ORDER BY order_date DESC |
完全匹配 + 索引排序 | 索引天然有序,避免额外排序操作 |
常见误区:不要将选择性低的列(如性别、状态码)放在复合索引的最左列,除非查询条件中该列是必选且等值条件。选择性低的列放在前面会导致索引树过早分叉,降低后续列的过滤效率。
3.2.2 排序与分组优化
如果查询包含 ORDER BY 或 GROUP BY,将排序列放在复合索引的末尾,且与 WHERE 条件中的列顺序一致,可以避免额外的排序操作。
-- 高效:索引 (department_id, hire_date) 同时满足 WHERE 和 ORDER BY
CREATE INDEX idx_emp_dept_hiredate ON employees (department_id, hire_date);
EXPLAIN ANALYZE
SELECT * FROM employees
WHERE department_id = 10
ORDER BY hire_date DESC;
-- 输出:Backward Index Scan using idx_emp_dept_hiredate(无需 Sort 节点)
-- 低效:索引 (hire_date, department_id) 无法直接用于排序
CREATE INDEX idx_emp_hiredate_dept ON employees (hire_date, department_id);
EXPLAIN ANALYZE
SELECT * FROM employees
WHERE department_id = 10
ORDER BY hire_date DESC;
-- 输出:Index Scan + Sort(额外排序开销)
3.3 冗余索引的识别与清理
冗余索引不仅浪费磁盘空间,还会拖慢 INSERT、UPDATE、DELETE 操作的性能,因为每次数据变更都需要同步维护所有索引。
3.3.1 冗余索引的常见类型
| 冗余类型 | 示例 | 说明 |
|---|---|---|
| 前缀冗余 | idx_a (a) 与 idx_ab (a, b) |
idx_a 的功能完全被 idx_ab 覆盖,可以删除 |
| 重复索引 | idx_a_b (a, b) 与 idx_b_a (b, a) |
列顺序不同,但可能部分功能重叠,需根据查询模式判断 |
| 未使用索引 | 创建后从未被查询计划使用 | 通过 pg_stat_user_indexes 检查使用频率 |
3.3.2 使用系统视图识别冗余索引
-- 1. 查找未被使用的索引(idx_scan = 0 表示从未被扫描)
SELECT
schemaname,
tablename,
indexname,
idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
-- 2. 查找前缀冗余索引(使用 pg_index 系统表)
SELECT
a.indrelid::regclass AS table_name,
a.indexrelid::regclass AS redundant_index,
b.indexrelid::regclass AS covering_index,
pg_size_pretty(pg_relation_size(a.indexrelid)) AS redundant_size
FROM pg_index a
JOIN pg_index b
ON a.indrelid = b.indrelid
AND a.indkey < b.indkey
AND a.indkey @> b.indkey -- a 的前缀包含 b 的所有列
WHERE a.indisprimary = false
AND b.indisprimary = false
AND a.indisunique = false
AND b.indisunique = false;
-- 3. 结合 pg_stat_user_indexes 确认冗余索引是否真的被覆盖
-- 如果 covering_index 的 idx_scan 远高于 redundant_index,则冗余索引可安全删除
清理策略:
- 在测试环境先执行
DROP INDEX ... CONCURRENTLY(不阻塞写操作) - 观察一周业务运行情况,确认无性能回退
- 在维护窗口执行正式删除,并回收磁盘空间
3.3.3 冗余索引清理的注意事项
- 唯一约束索引不可随意删除:即使功能上被覆盖,唯一索引还承担着数据完整性约束。
- 外键关联索引:PostgreSQL 不会自动为外键创建索引,但外键列上的索引对
ON DELETE CASCADE等操作至关重要,不要误删。 - 部分索引(Partial Index):如果冗余索引是部分索引,其过滤条件可能无法被普通复合索引替代,需单独评估。
最佳实践总结:
- 复合索引设计时,遵循等值列在前、范围列在后、排序列在最后的原则。
- 每张表的复合索引数量控制在 3~5 个以内,避免索引维护开销过大。
- 每季度执行一次冗余索引审计,使用
pg_stat_user_indexes和pg_index系统视图辅助分析。