并不是每一行都配得上一个索引槽位;部分索引让树尽量薄,正对查询从不关心的那些数据。多数表都有表结构假装看不见的偏斜:九成订单已关闭,几乎所有事件都已处理,标志列几乎处处为假。全量索引老老实实全记下来,每次写入都交维护费,保护的却是只碰一小片热数据的查询。部分索引不再为没人查的行付钱。
这笔交易拿精度换体积。索引更小、扫描更快、维护更便宜——但只有谓词与索引条件对得上的查询才能用它,其余统统回退到别的计划。所以部分索引是访问模式与存储的刻意配对,不是通用加速。只有热子集稳定、各查询对它的定义一致时才值得建。
只索引你要查的数据
部分索引带一个 WHERE 子句,不满足谓词的行根本不会进树。当大多数行很少被读时,树的体积因此更小,真正关心的那些行查找也更快。只给未关闭订单建的索引,可能比全量小一个数量级:更多内容装进缓存,每次查找碰更少的页,冷历史再怎么涨,计划依然走索引。
谓词从查询里挑,别从列统计里挑。正确的条件是每条重要查询本来就在重复的那一句:状态等于未关闭、删除时间为空。如果各查询切表的方式不同,一个部分索引伺候不了所有人——要么主导的那片胜出,要么建两个部分索引,每个都比全量瘦。定谓词之前,先让查询日志投票。
行更少、维护更省
条目越少,INSERT、UPDATE 还有 VACUUM 要碰的结构就越轻,磁盘占用也能肉眼可见地降下来。对写入频繁又大多是噪音的表,收益最明显。不满足谓词的插入整行跳过索引——没有页分裂风险,也不为它写日志;不改变谓词结果的更新同样跳过索引维护。对每秒吞噬几千冷写的表,省下的维护远超查询加速本身。
索引变小还会让清理更快:需要清理的页少了,膨胀无处藏身,表本来就要付的维护窗口跟着变短。上线后盯着它和全量等价物的体积比;部分索引一旦长到接近全量,说明谓词选出的早已不是少数,交易已经到期——删掉它,或重新定义切片。
部分索引还有一个容易漏掉的边界:规划阶段必须能证明查询条件蕴含索引谓词。把值写成预备语句参数,规划器无法对所有可能值提前证明,因此可能完全不用这个索引;不同写法的等价条件也未必能被识别。把热点条件固定成同一段查询构造,给带真实参数的 EXPLAIN 留一份回归样本,才能知道索引是在生产路径上工作,而不是只在建表演示里好看。
让语句与索引对得上
规划器通常只会为与部分索引谓词完全一致、或由其蕴含的查询选用它。所以查询里的过滤条件要照索引的写法写,规划器才能真正用得上。状态等于未关闭能命中条件相同的索引;状态属于两者之一则不行,哪怕人一眼能看出重叠。匹配器认的是逻辑蕴含,不是字面相似,而且设计上偏保守。
这份严格规定了一条纪律:全库统一热切片条件的写法。索引写的是删除时间为空,查询就不许写成删除时间等于空或包一层合并函数还指望走索引。把热条件收敛到一处——视图、作用域、查询构造器的片段——让每条语句继承逐字相同的谓词。执行计划是最终裁判:计划无视索引,说明两边谓词差在匹配器跨不过去的地方。
坏掉的样子
经典故障是谓词漂移:业务把"未关闭"重定义得多了一个状态,应用查询跟着改了,部分索引却悄悄对不上一半负载。不报错——计划一条一条退化成顺序扫描,延迟爬升,背锅的却找不到一次发布。上线时给核心查询加上计划断言,进持续集成或监控,断言计划里出现这个索引名。谓词漂移在发布当天就被抓住,而不是被发现的那个季度。
第二种故障是切太碎:一张表五个部分索引,各伺候一份报表,合计维护费超过当初要替换的全量索引。用索引统计视图审计:几乎零扫描的索引是删除候选,谓词重叠的是合并候选,合成一个更宽的部分索引。
快速验证
验证索引命中与体积收益:
CREATE INDEX ON orders (created_at) WHERE status = 'open';
EXPLAIN SELECT * FROM orders WHERE status = 'open' ORDER BY created_at LIMIT 20;
SELECT pg_size_pretty(pg_relation_size('orders_created_at_idx'));
计划必须走部分索引的索引扫描,体积应该是全量等价物的一小部分。
来源
- PostgreSQL 部分索引,部分索引官方文档,已核验 2026-06-01。
- PostgreSQL 执行计划,验证索引是否被选用的计划检查,已核验 2026-09-05。