3. 复合索引设计:最左前缀原则、索引列顺序选择、覆盖索引

复合索引(Composite Index / Multi-Column Index)是 PostgreSQL 中应对多条件查询的核心手段。与单列索引不同,复合索引的列顺序、查询条件的使用方式,直接决定了索引能否被有效利用。本章围绕三个核心概念展开:最左前缀原则、索引列顺序选择、覆盖索引。

3.1 最左前缀原则

PostgreSQL 的 B-Tree 复合索引按照索引定义的列顺序,从左到右构建一棵排序树。查询条件必须从索引的最左列开始匹配,跳过最左列或打乱顺序会导致索引无法被完整使用。

核心规则: 复合索引 (a, b, c) 可以匹配的条件组合:
  • ✅ WHERE a = 1 —— 使用索引
  • ✅ WHERE a = 1 AND b = 2 —— 使用索引
  • ✅ WHERE a = 1 AND b = 2 AND c = 3 —— 使用索引
  • ✅ WHERE a = 1 AND c = 3 —— 仅使用索引列 a,c 条件无法利用索引
  • ❌ WHERE b = 2 —— 无法使用索引
  • ❌ WHERE c = 3 —— 无法使用索引
复合索引 (a, b, c) 最左前缀匹配示意图 根节点:a 排序 中间节点:b 排序 叶子节点:c 排序 ✅ WHERE a = 1 AND b = 2 AND c = 3 → 完整匹配三层索引 ⚠️ WHERE a = 1 AND c = 3 → 仅匹配 a 层,c 层无法利用索引 ❌ WHERE b = 2 → 跳过最左列 a,完全无法使用索引
设计建议: 将等值查询条件中最常出现的列放在复合索引的最左侧。范围查询(>、<、BETWEEN)的列应放在等值列之后,因为范围条件会中断后续列的索引匹配。

3.2 索引列顺序选择

复合索引的列顺序不仅影响最左前缀匹配,还影响索引的选择性(Selectivity)和排序能力。选择正确的列顺序,可以大幅减少扫描的索引页数量。

场景 推荐顺序 原因
等值条件 + 等值条件 选择性高的列在前 快速过滤更多行,减少后续比较
等值条件 + 范围条件 等值列在前,范围列在后 等值列精确匹配后,范围列只需扫描连续区间
等值条件 + ORDER BY 等值列在前,排序列在后 索引天然有序,避免额外排序操作
高基数列 + 低基数列 高基数列在前 高基数列(如用户ID)能更快缩小数据范围
实战案例: 订单表查询 WHERE status = 'paid' AND created_at > '2024-01-01' ORDER BY created_at
  • ✅ 推荐索引:(status, created_at) —— 等值列 status 在前,范围+排序列 created_at 在后
  • ❌ 不推荐:(created_at, status) —— 范围条件在前,无法利用索引排序,且 status 等值过滤效果降低
索引列顺序选择对比 ✅ 推荐: (status, created_at) 等值列在前 → 精确过滤 范围列在后 → 连续扫描 + 免排序 ❌ 不推荐: (created_at, status) 范围列在前 → 无法利用索引排序 等值列在后 → 过滤效果降低 💡 选择原则:等值条件列 > 范围条件列 > ORDER BY 列 > GROUP BY 列 高选择性列优先,减少索引扫描深度

3.3 覆盖索引

覆盖索引(Covering Index / Index-Only Scan)是指索引中包含了查询所需的所有列,PostgreSQL 可以直接从索引页获取数据,无需回表访问堆表(Heap)。这能显著减少 I/O,尤其适合高频只读查询。

关键条件:
  • 查询的 SELECT 列、WHERE 条件列、ORDER BY 列全部包含在同一个复合索引中
  • 索引的可见性映射(Visibility Map)标记为全可见,否则仍需回表检查事务可见性
查询类型 索引设计 是否覆盖 执行计划
SELECT id, name FROM users WHERE status = 1 (status, id, name) ✅ 是 Index Only Scan
SELECT id, name, email FROM users WHERE status = 1 (status, id, name) ❌ 否(缺少 email) Index Scan + 回表
SELECT count(*) FROM orders WHERE status = 'paid' (status) ✅ 是(仅索引列) Index Only Scan
性能收益: 覆盖索引可减少 50%~90% 的 I/O 开销,因为索引页通常比堆表页更紧凑,且 B-Tree 的缓存命中率更高。在报表导出、热点数据查询等场景中效果尤为明显。
-- 创建覆盖索引示例
CREATE INDEX idx_orders_status_date_amount 
ON orders (status, created_at, amount);

-- 查询:所有列都在索引中,触发 Index Only Scan
EXPLAIN (ANALYZE, BUFFERS) 
SELECT status, created_at, amount 
FROM orders 
WHERE status = 'shipped' 
  AND created_at > '2024-06-01';
注意事项:
  • 覆盖索引会增加索引体积,因为需要存储更多列数据
  • 写操作(INSERT/UPDATE/DELETE)的维护成本会上升
  • 仅对高频只读查询设计覆盖索引,避免过度索引
  • PostgreSQL 的 Index Only Scan 依赖可见性映射,频繁更新的表可能无法完全避免回表

3.4 综合对比:三种设计策略

策略 核心目标 适用场景 代价
最左前缀原则 确保索引能被查询条件匹配 多条件组合查询 列顺序固定,灵活性受限
索引列顺序选择 最大化过滤效率与排序能力 等值+范围+排序混合查询 需要分析查询模式
覆盖索引 避免回表,减少 I/O 高频只读查询、报表导出 索引体积增大,写性能下降