PostgreSQL RDS中索引与顺序扫描性能差异及generate_series影响的技术咨询
问题1:为什么禁用顺序扫描(使用索引)反而比顺序扫描快2倍?
咱们从数据分布和执行计划细节拆解下:
首先你的表数据是按content_id聚类的(每个content_id对应100行连续数据),当你用IN列表指定10K个content_id时,实际只需要读取100万行,但顺序扫描是遍历整个1亿行表,对每一行都要检查content_id是否在列表里——从执行计划能看到,每个worker要过滤掉3300万行,三个worker总共过滤9900万行,这个CPU过滤的开销极大。
另外,顺序扫描的I/O成本也很高:原计划里I/O Timings: shared/local read=1027.617,说明大量数据不在内存缓存里,需要从磁盘读取全表数据,这进一步拖慢了速度。
而当你禁用顺序扫描后,PostgreSQL选择了Parallel Bitmap Heap Scan + Bitmap Index Scan的组合:
- 索引扫描会一次性定位所有符合
IN列表的content_id对应的行位置,生成一个位图; - 然后批量读取这些行所在的磁盘块(计划里
Heap Blocks: exact=64444,只读取需要的块,不是全表); - 此时数据大概率已经在内存缓存里了(
Buffers: shared hit=95578没有磁盘读取),不需要额外的磁盘I/O开销。
简单说:顺序扫描是“先读全表再过滤”,索引扫描是“先定位目标再读数据”,你的场景里目标数据只占全表的1%,后者的效率自然高很多。
问题2:为什么换成generate_series的IN子查询后,索引扫描的优势消失了?
核心原因是执行计划的逻辑完全变了:
看第三个查询的执行计划,PostgreSQL选择了Nested Loop的处理方式:
- 先生成
generate_series的10K个content_id值; - 对每个值单独执行一次
Bitmap Index Scan和Bitmap Heap Scan——也就是循环10K次,每次只查一个content_id的100行。
这种逐个处理的方式,把原本可以批量完成的索引查询拆成了10K次小查询,虽然每次查询的开销很小,但累积起来就很大:比如每次索引扫描耗时0.005ms,10K次就是50ms,加上每次堆扫描的开销,再加上嵌套循环的调度成本,整体耗时就回到了和顺序扫描差不多的水平。
而之前禁用顺序扫描的查询,是把IN列表作为一个数组,用= ANY()的方式让PostgreSQL一次性处理所有目标content_id,生成一个合并后的位图,批量读取数据,没有循环的额外开销。
如果想让generate_series的查询也用上高效的批量索引扫描,可以尝试把子查询转成数组:
WHERE content_id = ANY(ARRAY(SELECT generate_series(0, 999999, 100)))
这样优化器更可能选择批量Bitmap Scan的计划,复用之前的高效路径。
备注:内容来源于stack exchange,提问作者AlwaysLearning

