PostgreSQL为何在该示例中不联合使用两个索引?
首先,咱们拆解下你的场景和查询逻辑:你通过计算字段last_event_id模拟订单簿事件快照,创建了表t并建立了两个索引——基于event_id的主键索引t_pkey,以及基于last_event_id的btree索引t_idx。执行查询SELECT * FROM t WHERE event_id <= 80001 AND last_event_id >= 80001;后,查询计划只走了t_pkey索引,过滤掉了绝大多数行,却没联合使用t_idx。这背后主要是PostgreSQL优化器的成本估算逻辑和数据分布特性在起作用,具体原因如下:
1. 优化器认为单索引扫描+过滤的成本更低
PostgreSQL的查询优化器会对比所有可行执行计划的成本,选择代价最小的方案。对于你的查询:
- 如果选择联合两个索引(比如Bitmap Index Scan),需要先分别从
t_pkey和t_idx中获取符合条件的行的位图,再对两个位图做交集运算,最后根据位图去表中读取数据。这个过程涉及多次索引扫描、位图运算和回表,优化器估算下来的总成本可能更高。 - 而选择单走
t_pkey索引,直接扫描所有event_id <= 80001的行(共80001行),然后在内存中过滤出last_event_id >= 80001的行(仅2行),优化器认为这个方案的IO和CPU成本更低——毕竟扫描8万行的开销,加上简单的过滤操作,比多索引联合的复杂流程更划算。
2. last_event_id的索引选择性不足
从你的last_event_id计算逻辑10000*(round(i.event_id/10000,0)+1)来看,这个字段的取值是分段的(一批连续的event_id对应同一个last_event_id),意味着每个last_event_id值会对应大量行,索引的选择性极低。优化器会认为这种低选择性的索引,单独使用或联合使用的价值都不高,不值得为它额外付出索引扫描和位图运算的成本。
3. PostgreSQL 9.6版本的优化器局限性
PostgreSQL 9.6是2016年发布的老版本,其优化器在多索引联合查询的成本估算上,相比新版本(比如12+)不够精准。对于这种满足条件的行数极少、且其中一个索引(主键索引)扫描范围明确的场景,老版本优化器更倾向于选择简单直接的单索引扫描+过滤方案,而不会尝试更复杂的多索引联合。
可以尝试的优化方案
如果你想让查询更高效,可以考虑创建复合索引,让优化器在索引层面完成过滤:
CREATE INDEX t_event_last_idx ON t(event_id, last_event_id);
这个复合索引可以让优化器在扫描event_id <= 80001的过程中,直接在索引层面过滤last_event_id >= 80001的条件,避免回表后再做大量过滤操作,进一步降低查询成本。
或者,如果你想验证多索引联合的效果,可以临时关闭单索引扫描的开关,强制优化器考虑Bitmap扫描:
SET enable_indexscan = off; EXPLAIN ANALYZE SELECT * FROM t WHERE event_id <= 80001 AND last_event_id >= 80001; SET enable_indexscan = on;
对比两种计划的执行时间,就能直观看到哪种方案更适合你的数据。
内容的提问来源于stack exchange,提问作者zer0hedge

