索引类型与结构
PostgreSQL 把索引抽象为 Access Method,并通过 Operator Class 定义某种数据类型的哪些操作符能够使用该索引。选择索引不能只看列类型,还要看查询谓词、操作符、数据分布和写入成本。
内置访问方法
| 类型 | 结构与定位 | 适合的访问 |
|---|---|---|
| B-tree | 有序平衡树,默认类型 | 等值、范围、排序、前缀模式匹配 |
| Hash | 保存键的哈希码 | 单列等值比较 |
| GiST | 平衡搜索树框架,由 Operator Class 定义语义 | 几何、范围、最近邻和可扩展搜索 |
| SP-GiST | 空间分区搜索树框架 | 四叉树、k-d tree、Trie 等非平衡数据结构 |
| GIN | 倒排索引,键映射到包含它的行集合 | 数组、JSONB、全文词项、多值包含查询 |
| BRIN | 汇总连续 Heap Block Range 的值域 | 超大表、列值与物理顺序高度相关的数据 |
GiST、SP-GiST 和 GIN 是可扩展框架,同一访问方法在不同 Operator Class 下支持的操作符不同。看到索引类型名称不能直接推断某个谓词一定可用。
B-tree 与 Heap
PostgreSQL 普通表是 Heap,B-tree 叶子项保存索引键和指向 Heap Tuple 的 TID。表数据不会像聚集索引组织表那样直接存放在主键索引叶子中,因此通过普通索引取完整行通常还要访问 Heap。
B-tree Root / Internal Page
→ Leaf Entry(Key + TID)
→ Heap Page
→ Tuple Version表的物理顺序可以通过 CLUSTER 按某索引一次性重排,但后续写入不会自动持续维持这种顺序。它与始终按聚集键组织数据的聚集索引不是同一机制。
Index-Only Scan
INCLUDE 可以把仅用于返回、不用于搜索的列放入支持覆盖列的索引。即使查询列全部位于索引,PostgreSQL 仍需确认 Heap Tuple 对当前快照可见;只有 Visibility Map 表明对应 Heap Page 为 all-visible 时,才能跳过 Heap 访问。
覆盖索引的收益取决于查询列、页面可见性、VACUUM 维护速度以及索引变宽后的写入成本。高频更新表的 Visibility Map 会不断被清除,Index-Only Scan 仍可能产生大量 Heap Fetch。
组合、表达式与部分索引
| 形式 | 作用 | 关键边界 |
|---|---|---|
| 多列索引 | 对组合条件、排序提供有序访问 | B-tree 通常最依赖左侧列形成的约束;列顺序需按真实谓词设计 |
| 表达式索引 | 索引函数或表达式结果 | 查询表达式需匹配定义,函数必须满足不可变性要求 |
| 部分索引 | 只索引满足谓词的行 | 优化器必须能证明查询条件蕴含索引谓词 |
| 唯一索引 | 强制键值唯一 | 唯一约束通常由唯一 B-tree 实现;NULL 语义需按定义确认 |
| INCLUDE | 保存只用于返回的覆盖列 | 覆盖列不参与搜索,会增加索引体积 |
优化器也可以组合多个索引生成 Bitmap Scan。多个单列索引有时能组合过滤,但会丢失原有索引顺序,也不一定优于专门的复合索引。
特殊访问方法的选择
| 问题 | 常见选择 | 原因 |
|---|---|---|
| JSONB 包含某个键值 | GIN | 把复合值拆成检索键并维护 Posting List |
| 全文包含词项 | GIN | 适合词项到文档集合的倒排访问 |
| 几何相交或最近邻 | GiST / SP-GiST | 由空间 Operator Class 提供重叠、距离等语义 |
| 时间递增的超大日志表范围查询 | BRIN | 索引极小,可按块范围排除数据 |
| 更新频繁的小型等值表 | B-tree | 通用、选择性定位稳定 |
BRIN 返回的是可能匹配的块范围,需要 Recheck,不适合物理顺序和列值无相关性的随机数据。GIN 写入和维护成本通常高于 B-tree,不能因查询快就忽略写路径。
维护判断
- 用
EXPLAIN (ANALYZE, BUFFERS)区分估算问题、访问路径问题和真实 I/O。 - 统计信息不准确时,合理索引也可能不被选择。
- 长事务与 VACUUM 积压会使 Heap 和索引保留无效条目。
- 并发重建仍会消耗 CPU、I/O、WAL 和额外磁盘空间。
- 删除重复索引前检查约束依赖、写入成本和完整业务周期内的使用情况。
顺序扫描不一定是异常。当查询需要表中较大比例的数据时,连续扫描可能比大量随机索引访问更便宜。