症状
执行计划会直接告诉你属于四种成因中的哪一种:Filter 上包着 lower(email) 说明函数禁用了普通 btree;estimated rows 5、actual rows 500000 说明统计在编小说;列与参数类型不一致会看到隐式 cast;而谓词命中全表三成时 Seq Scan 本身就是正确答案。用 EXPLAIN (ANALYZE, BUFFERS) 看计划形状与估算-实际偏差,四种病因各有各的形状,每个形状自带修法。
诊断思路
先读计划形状:Filter 表达式包列指向函数或 cast 问题,Rows Removed by Filter 巨大且估算偏差大指向统计问题;再对比估算与实际行数,相差数量级即统计失真;最后检查谓词表达式与列类型,函数或 text-integer 错配一眼可见。批量加载后性能随时间劣化、ANALYZE 后又恢复,是统计过期的行为指纹。
根因
规划器只在“谓词可由索引序与键比较回答”且“成本模型偏好索引”时才使用索引。对被索引列施加函数、跨类型隐式 cast、或统计失真,都会打破前提之一,规划器退回顺序扫描。
修复与验证
按病因修复:表达式谓词配表达式索引 CREATE INDEX … (lower(email));参数类型与列类型对齐(绑定正确类型);批量变更后 ANALYZE、倾斜列提高 STATISTICS 目标;真正低选择性的谓词接受 Seq Scan——调查询而不是调计划。怀疑时用 SET LOCAL enable_seqscan = off 的会话级实验验证索引可行性,看完即回滚。验证时 EXPLAIN ANALYZE 显示索引扫描(或有正当理由的顺序扫描),估算行数与实际行数在小倍数内吻合。全局关掉 enable_seqscan 会腐蚀所有其他查询的成本模型,只做本地实验。
代码示例
EXPLAIN ANALYZE SELECT * FROM users WHERE lower(email) = '[email protected]';
Seq Scan on users (cost=0.00..98214.00 rows=1 width=...)
Filter: (lower((email)::text) = '[email protected]'::text)
Rows Removed by Filter: 1000000
CREATE INDEX users_email_lower ON users (lower(email));
ANALYZE users;
cur.execute("SELECT ... WHERE tenant_id = %s", (tenant_id,)) # int param, int column
ANALYZE big_table;
ALTER TABLE big_table ALTER COLUMN status SET STATISTICS 500; -- skewed column
BEGIN; SET LOCAL enable_seqscan = off; EXPLAIN ANALYZE ...; ROLLBACK;