在排查PostgreSQL慢查询时,很多同学的直觉是给查询条件涉及的列加一个B-tree索引。但有一种情况值得注意:表中的数据分布严重倾斜,比如一张订单表里95%的记录都是已完成状态,而所有高频查询都只盯着那5%的未完成订单。这时候给整个状态列建全量索引,索引里绝大部分条目都是永远用不上的死数据,既浪费磁盘空间,又拖慢写入。PostgreSQL提供的部分索引正好能解决这个问题,它只对满足特定条件的行建立索引,让索引体积和查询需求精确匹配。

什么是部分索引,它和普通索引有什么区别
部分索引的英文叫partial index,本质上就是在CREATE INDEX语句后面附加一个WHERE子句,告诉PostgreSQL只把满足条件的行放入索引。普通索引会为表中每一行都维护一条索引记录,而部分索引只收录符合条件的行子集。这个看似简单的差别,在数据分布倾斜的场景下能带来数量级的收益。
举个典型的例子,一张存储了几千万行的消息表,其中绝大部分消息都已读,未读消息只占很小的比例。业务上所有活跃查询都在找某个用户的未读消息,比如这样的SQL:
-- 普通索引:为所有行建立索引,体积大
CREATE INDEX idx_messages_user ON messages(user_id);
-- 部分索引:只索引未读消息
CREATE INDEX idx_messages_unread
ON messages(user_id)
WHERE is_read = false;
第二条语句创建的索引只包含未读消息的条目,索引可能从原来的几个GB缩小到几十MB。索引变小之后,一方面顺序扫描索引的成本降低,查询更快;另一方面每次INSERT、UPDATE时需要维护的索引页更少,写入路径的开销也相应下降。对于那种条件固定、只查询子集的场景,部分索引几乎是免费的性能提升。
规划器如何决定是否使用部分索引
理解部分索引的匹配规则非常重要,因为规划器并不会随意使用它。PostgreSQL只在外层查询的WHERE条件能够逻辑上蕴含部分索引的WHERE条件时,才会考虑使用这个索引。简单说,就是查询条件必须比索引条件更严格或至少等价,确保索引中包含了查询需要的所有行。
拿上面的索引举例,下面这些查询都能用上索引:
-- 条件与索引条件完全一致,可以走部分索引 SELECT * FROM messages WHERE user_id = 42 AND is_read = false; -- 条件比索引更严格,同样可以走部分索引 SELECT * FROM messages WHERE user_id = 42 AND is_read = false AND created_at > now() - interval '7 days';
但如果查询写成下面这样,规划器就无法使用索引了:
-- 没有包含 is_read = false 条件,规划器不敢用索引 -- 因为索引里没有已读消息,走索引会漏掉数据 SELECT * FROM messages WHERE user_id = 42; -- 这样也不行,条件与索引条件矛盾 SELECT * FROM messages WHERE user_id = 42 AND is_read = true;
这里有一个实际开发中容易踩的坑:应用层传参时如果把布尔条件写成参数化的形式,比如is_read = $1,在PREPARE语句的场景下,通用计划可能无法证明条件蕴含关系,导致索引失效。遇到这种情况可以用EXPLAIN确认执行计划,必要时改写SQL让条件显式出现在查询文本中。另外,索引条件里用到的表达式如果涉及函数或时区转换,匹配判断也会变得更复杂,尽量保持索引条件简单直接。
适合使用部分索引的典型业务场景
第一个经典场景是软删除。很多系统的表都有deleted_at或is_deleted字段,业务查询统一带上未删除的条件。给这类表建部分索引效果非常好:
-- 只为未删除的数据建立索引
CREATE INDEX idx_orders_active
ON orders(customer_id, created_at)
WHERE deleted_at IS NULL;
-- 典型查询
SELECT * FROM orders
WHERE customer_id = 1001
AND created_at >= '2024-01-01'
AND deleted_at IS NULL;
第二个场景是任务队列或工单系统。任务表中绝大部分记录都是历史完成态,活跃任务只占少数,处理逻辑反复扫描活跃任务。这时候按状态条件建部分索引,轮询查询的开销可以稳定在一个很低的水平,不会随着历史数据膨胀而劣化。
第三个场景是排除测试数据或异常值。比如日志表里某个内部服务的调试日志占了70%的空间,而分析查询从不关心它们,可以用WHERE source != 'debug'这样的条件建部分索引,让分析查询的索引干净利落。反过来讲,如果查询条件五花八门,没有任何一组固定条件被高频使用,那么部分索引的适配空间就不大,老老实实建全量索引更稳妥。
使用部分索引的注意事项与维护建议
首先要注意索引条件的可变性。部分索引依赖建索引时的WHERE条件,如果业务查询口径发生变化,比如未完成订单的定义从两个状态变成三个状态,旧的部分索引就不会再被命中,必须同步重建。建议把索引条件纳入团队的知识沉淀,SQL规范中明确写出哪些查询必须携带哪些条件,避免后来者写出不走索引的SQL而不自知。
其次,部分索引和唯一约束结合时有一个很实用的技巧。唯一部分索引可以在子集范围内强制唯一性,例如只要求每个用户有一条未完成的支付单:
-- 每个用户同时只能有一条 pending 状态的支付单
CREATE UNIQUE INDEX uniq_active_payment
ON payments(user_id)
WHERE status = 'pending';
这种写法绕过了普通唯一约束必须全表唯一的限制,实现起来非常优雅。最后别忘了用pg_stat_user_indexes视图观察索引的使用情况,如果发现某个部分索引长期没有扫描命中,及时清理掉。配合EXPLAIN和pgbench做前后对比,用数据验证优化效果,而不是凭感觉加索引。索引不是越多越好,部分索引的价值恰恰在于精准和克制。
PostgreSQL慢查询优化部分索引partial index修改时间:2026-09-13 08:28:28