为什么SQL连接查询速度突然变慢?查看执行计划定位Index Scan失效

执行计划里 Index Scan 变成了 Seq Scan,就是性能掉下去的直接原因
在PostgreSQL(或兼容引擎)里,如果发现原本好好的Index Scan突然变成了Seq Scan,那查询变慢的“罪魁祸首”基本就找到了。这可不是什么偶然事件,而是优化器经过一番“精打细算”后,主动放弃了索引——它觉得走索引反而更“贵”。问题的核心不在于索引失效,而在于优化器的“判断”变了。什么情况下它会这么判断呢?常见诱因有几个:表数据量突然暴涨、WHERE条件的选择率发生变化、统计信息过期没更新,或者隐式类型转换导致索引无法被有效利用。
EXPLAIN (ANALYZE, BUFFERS) 必须带 ANALYZE 才能看到真实行为
这里有个关键点:只用EXPLAIN看到的只是优化器的“预估”计划,而EXPLAIN ANALYZE会真正去执行这条语句,把实际的耗时、扫描行数、缓冲区命中情况都摆在你面前。输出结果里,要特别关注两个地方:Actual Rows和Rows Removed by Filter。如果后者的数值特别大(比如扫描了100万行,结果过滤掉了99.9万),那基本可以断定,查询条件没有在索引层完成过滤,很可能走了全表扫描,或者索引只起到了定位作用,大量的过滤工作都留给了CPU去处理。
具体可以这么做:
- 在业务低峰期运行
EXPLAIN (ANALYZE, BUFFERS) SELECT ...,避免对线上服务造成影响。 - 对比问题发生前后的执行计划,重点看
Plan Node的类型、Startup Cost和Total Cost这几个代价估算值有没有大幅跳升。 - 仔细检查
Buffers: shared hit=xxx read=yyy这一行——如果read的值很高,通常意味着磁盘IO暴增,这往往是伴随Seq Scan出现的典型信号。
为什么 WHERE col = '123' 没走索引?先查隐式转换
这个问题非常隐蔽,但发生频率却很高:假设字段是integer类型,但查询时写成了WHERE id = '123'(用了字符串字面量)。PostgreSQL会尝试把字符串转换成整数,但这个转换动作发生在运行时,导致索引无法被使用。原因很简单,索引是按整数结构建立的,而查询条件被包装成了CAST('123' AS integer),优化器无法将这个表达式与索引匹配。
怎么验证呢?
- 用
\d table_name命令查看列的确切数据类型。 - 在
EXPLAIN的输出里寻找Filter:这一行,如果看到类似(col = ('123')::integer)的写法,那就是发生了隐式转换的铁证。 - 修复方法很简单:统一类型即可,把查询改成
WHERE id = 123(去掉引号)。
同样的逻辑也适用于其他场景,比如用字符串去比较timestamp字段,或者查询jsonb字段时用了->操作符(返回jsonb类型)而不是->>(返回文本类型)导致类型不匹配。
VACUUM ANALYZE 不是“重启大法”,但往往是最快见效的干预
当一张表刚刚经历大批量的INSERT、UPDATE或DELETE操作后,统计信息如果没及时更新,就会严重滞后,导致优化器错误地判断索引的价值。这时候,单独执行ANALYZE就能刷新列的分布直方图,让优化器“看清”现状。如果同时还存在大量未被清理的死元组(比如更新操作产生的旧版本数据),那么就需要VACUUM ANALYZE一起上阵了。
有几点需要注意:
ANALYZE操作不会锁表,但它会短暂持有ShareUpdateExclusiveLock锁,通常不会影响正常的读写。- 不要盲目地对所有大表执行
ANALYZE,可以指定关键列来加速,例如:ANALYZE table_name (col1, col2)。 - 如果统计信息频繁失真,可以考虑调高
default_statistics_target参数(默认值是100,可以尝试设为500),再配合定期的ANALYZE任务。
当然,真正棘手的是多列组合条件下的选择率误估问题。这时候,可能就需要创建扩展统计(CREATE STATISTICS)或者考虑使用函数索引了,不过那又是另一个层面的复杂度了。
