PostgreSQL 部分索引:索引里存什么,决定它能不能被用上

举报
yd_228692151 发表于 2026/11/08 06:42:22 2026/11/08
【摘要】 PostgreSQL 部分索引:索引里存什么,决定它能不能被用上一张订单表跑到两千万行,其中 99.3% 的行状态是 paid 或 closed,只有十几万行是 pending。后台的待处理队列每隔几秒查一次 status = 'pending',走全表索引既浪费空间又拖慢写入。给 status 加部分索引是标准做法,但写法有讲究——写对了查询能用上,写错一个字符就完全失效,而且计划器不会...

PostgreSQL 部分索引:索引里存什么,决定它能不能被用上

一张订单表跑到两千万行,其中 99.3% 的行状态是 paid 或 closed,只有十几万行是 pending。后台的待处理队列每隔几秒查一次 status = 'pending',走全表索引既浪费空间又拖慢写入。给 status 加部分索引是标准做法,但写法有讲究——写对了查询能用上,写错一个字符就完全失效,而且计划器不会有任何警告。

先看完整索引的实际代价

CREATE INDEX idx_orders_status ON orders (status);

两千万行里 status 只有五种取值,选择度极低。按 status = 'pending' 查询时,优化器大概率不会用这个索引——它要扫两千万条索引项才能捞出十几万行,再回表,成本比顺序扫描还高。索引白建,还让每次 INSERT 多一次 B-tree 维护。

换成部分索引:

CREATE INDEX idx_orders_pending ON orders (created_at)
  WHERE status = 'pending';

索引项直接从两千万降到十几万,体积小了约两个数量级,写入维护成本同步下降。查询改成:

SELECT id, created_at FROM orders
WHERE status = 'pending' AND created_at > now() - interval '1 hour'
ORDER BY created_at;

EXPLAIN 里应看到 Index Scan using idx_orders_pending,Index Cond 带 created_at > ...,且没有 Filter: (status = 'pending')。如果 Filter 还在、走的是 Seq Scan,索引没被识别。

失效的第一种原因:谓词没有严格蕴含

部分索引的可用条件是查询的 WHERE 必须逻辑上蕴含索引的 WHERE。上面那条查的是 status = 'pending' AND ...,蕴含成立。下面这些改法都会让它失效:

-- 常量类型不一致:status 是 varchar,'pending' 被强制转换,蕴含判断失败
WHERE status = 'pending'::text AND ...
-- 用 IN 列出多个状态,不蕴含单值条件
WHERE status IN ('pending', 'paying') AND ...
-- 用不等号,更不蕴含
WHERE status <> 'closed' AND ...

第二种特别容易踩:业务上想把 pending 和 paying 一起查,顺手写成 IN。这时代码逻辑没错,但索引用不上了。要么拆成两条 SQL 用 UNION ALL,要么把索引改成 WHERE status IN ('pending','paying')——注意索引谓词里的表达式必须和查询里的写法字面一致,IN ('pending', 'paying') 与 IN ('paying', 'pending') 在语义上等价,但计划器做蕴含推理时按语法树匹配,顺序不同不一定能匹配上,稳妥做法是两边照抄同一段。

第二种原因:函数包裹了索引列

CREATE INDEX idx_orders_pending ON orders (lower(status))
  WHERE status = 'pending';

索引键是 lower(status),查询用的是 status = 'pending',蕴含关系不成立,索引直接不可用。要么查询也写 lower(status),要么索引键就用原始列。这类问题在线上最难发现,因为 CREATE INDEX 成功、\d orders 里也能看到这个索引,只有 EXPLAIN 会暴露它从未被使用。

第三种原因:模型统计信息过期

部分索引的可用量估算依赖 pg_statistic 里对谓词的记录。大量数据增删后,pending 行的真实占比变化很大,如果 ANALYZE 没跟上,计划器可能错误地认为这个索引扫出来的行数太多,转而选择顺序扫描。先跑一次 ANALYZE orders; 再看计划,如果之前是 Seq Scan、之后变成 Index Scan,问题就在统计信息。

怎么确认索引真被用上了

不要凭 CREATE INDEX 成功就认为生效。三个可验证的手段:

-- 1) 看计划,重点是没有 Filter: (status = 'pending')
EXPLAIN (ANALYZE, BUFFERS)
SELECT ... WHERE status = 'pending' AND ...;

-- 2) 看索引的实际体积与扫描次数
SELECT indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes WHERE relname = 'orders';

-- 3) 强制关闭顺序扫描做对照(仅调试环境)
SET enable_seqscan = off;
EXPLAIN SELECT ...;

第 2 条里的 idx_scan 是累计扫描次数。上线一段时间后它仍是 0,说明这个索引一次都没被用过,要么谓词不匹配,要么统计信息让计划器放弃了它。

一个容易被忽略的副作用

部分索引只覆盖满足谓词的行,所以不能用它来支撑其他条件的查询。如果同一条 SQL 里还有 AND user_id = ?,索引里没有 user_id,user_id 就只能作为过滤条件在回表后执行。行数少时没问题,行数多时要权衡是否建成复合部分索引:

CREATE INDEX idx_orders_user_pending ON orders (user_id, created_at)
  WHERE status = 'pending';

这种写法把选择度最高的列放前面,再按时间排序,可以同时服务"某用户的待处理订单"和"全部待处理订单按时间"两类查询(后者利用索引前缀的 created_at 排序时会不够理想,因为 user_id 在前打断了全局有序)。

小结式的判据

判断一个部分索引是否值得建、是否真的生效,按这个顺序看:

检查项 期望结果 不满足时
索引谓词字面包含在查询谓词里 查询 WHERE 里有完全相同的子表达式 改写查询或索引
索引键列未被函数包裹 查询里对同一列做同样的包裹 统一写法
EXPLAIN 中无 Filter: <索引谓词> 索引条件直接出现在 Index Cond 谓词不匹配或统计过期
pg_stat_user_indexes.idx_scan 上线后持续增长 索引未被使用,考虑删除
索引体积与全表索引对比 明显更小 谓词选择性不够

部分索引的价值不在"少写一个 WHERE",而在于把索引从"全表数据的副本"变成"只包含关注数据的结构"。它的失效方式又格外安静——不报错、不警告,只在执行计划里少一行。所以建完之后的那次 EXPLAIN 不是可选动作。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。