SQL 为什么没有走索引:用 EXPLAIN 读懂执行计划并定位慢查询

用执行计划诊断 SQL

慢查询并不总是“少了一个索引”。优化器会在顺序扫描、索引扫描和各种连接算法之间估算成本;即使索引存在,它也可能判断直接读表更便宜。与其凭感觉加索引,不如先读取执行计划,确认数据库实际做了什么,再用数据验证改动是否有效。

先区分计划与实测

以 PostgreSQL 为例,EXPLAIN 展示优化器准备采用的计划,EXPLAIN ANALYZE 会真正执行 SQL,并补充时间、实际行数与循环次数。后者更接近现场,但会产生真实读写;对 UPDATEDELETE 等语句,应在事务中执行后回滚,或在安全副本上分析。

1
2
3
4
5
6
7
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, total_cents
FROM orders
WHERE tenant_id = 42
AND status = 'paid'
ORDER BY created_at DESC
LIMIT 50;

拿到结果后,先读最耗时的节点,而不是只看最上面的 Limit。常见节点含义如下:

节点 表示什么 需要追问的问题
Seq Scan 顺序读取整张表 过滤条件是否足够少、统计信息是否准确?
Index Scan 经索引定位后回表取数据 索引列顺序是否匹配过滤和排序?
Bitmap Heap Scan 汇总多个索引命中后批量回表 命中行是否太多,导致回表成本上升?
Nested Loop 外层每一行驱动一次内层查找 外层实际行数会不会被低估?
Hash Join 建哈希表后匹配两侧数据 内存是否够用,是否出现临时磁盘读写?

cost 是优化器内部的相对成本,不是毫秒;真正要对照的是预估 rowsactual rows。若预估 10 行而实际返回 10 万行,后续的连接、排序或循环都会沿着错误的规模决策。此时盲目创建索引通常治标不治本,先检查统计信息、数据分布与条件写法。

顺着执行树找到代价来源

SQL 查询计划流向

执行计划是一棵树:叶子节点读取表或索引,父节点完成过滤、连接、聚合与排序。actual time 的结束时间、rowsloops 要一起看;一个看似只花 2ms 的节点,如果循环 1,000 次,累计成本就不小。

下面这种查询常被误判为“索引失效”:

1
2
3
SELECT *
FROM orders
WHERE created_at::date = DATE '2026-08-01';

对列做函数或类型转换,可能让普通的 created_at 索引无法直接限定范围。若业务语义是某一天,应改成可搜索的半开区间:

1
2
3
4
SELECT *
FROM orders
WHERE created_at >= TIMESTAMP '2026-08-01 00:00:00'
AND created_at < TIMESTAMP '2026-08-02 00:00:00';

同类陷阱还有前置通配符的 LIKE '%error'、隐式类型转换,以及把多个互斥条件塞进复杂表达式。先将条件改写为能缩小原始列范围的形式,再重新执行同一条 EXPLAIN ANALYZE,才能把效果归因到改动本身。

按访问路径设计复合索引

索引与全表扫描的选择

索引应服务于稳定的访问路径,而不是为每一列各建一个。对于“租户和状态等值过滤,再按时间倒序取前 50 条”的查询,通常可以从等值过滤列开始,再放范围或排序列:

1
2
CREATE INDEX CONCURRENTLY idx_orders_tenant_status_created_at
ON orders (tenant_id, status, created_at DESC);

这并非放之四海皆准的列顺序。选择性很低的 status 单独放在索引前部常常帮助有限;而当查询只按 tenant_id 和时间过滤时,上面的索引仍可能可用。是否需要覆盖列、部分索引或表达式索引,取决于实际查询、写入频率与存储成本。每增加一个索引,都要承担写入维护、真空清理和迁移时间,不能把它当作免费开关。

建索引后不要只确认节点名称从 Seq Scan 变成 Index Scan。在相同参数和相近数据量下复测总耗时、共享缓冲区命中与返回行数;如果原查询本来就要读取大部分表,顺序扫描反而可能是正确选择。

一套可复用的诊断闭环

把慢查询排查变成固定流程,能避免“生产环境看到一个计划就立刻改索引”的冲动:

步骤 操作 产出
固定样本 记录参数、返回行数和数据量 可复现的 SQL
采集计划 在安全环境运行 EXPLAIN ANALYZE 执行树与实测指标
找偏差 对比预估行数和实际行数 错误假设或瓶颈节点
小步改动 改写条件、更新统计或增加索引 单一可解释的变更
回归验证 用同一输入复测,并观察写入代价 前后对比记录

最后,把代表性计划、查询参数范围和索引意图写进变更说明。执行计划会随着数据规模与分布变化,今天合理的索引未来也可能变得多余。持续保留“为什么这样设计”的证据,才能让 SQL 优化从一次救火,变成团队可复用的诊断能力。