2. 读懂执行计划

读懂执行计划:识别全表扫描与索引失效 学习卡片

执行计划里到底该看什么

对一条慢查询执行 EXPLAIN,核心不是看它“有没有用索引”,而是看优化器为每张表选择的访问路径:是全表扫描、索引范围扫描、索引等值查找,还是先走二级索引再回表。判断顺序建议固定为:先看 type 判断访问类型,再看 key 判断实际用了哪个索引,再看 rows 估算扫描行数,最后看 Extra 判断过滤、排序和回表代价。

一条典型的慢查询:
SELECT id, user_id, amount, created_at FROM orders WHERE DATE(created_at) = '2024-05-01' AND status = 1;
该写法对 created_at 使用了函数,索引列被包裹后无法直接用于定位,优化器只能退化为全表扫描。

用 EXPLAIN 定位访问路径

下面给出该查询在 MySQL 8.0 上的执行计划示例(示例数据,非真实运行结果)。重点观察 type=ALL、key=NULL、rows 接近全表行数、Extra 中出现 Using where,这四项组合就是全表扫描的典型特征。

EXPLAIN
SELECT id, user_id, amount, created_at
FROM orders
WHERE DATE(created_at) = '2024-05-01'
  AND status = 1;

+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | orders | ALL  | idx_status    | NULL | NULL    | NULL | 986420 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+

三类典型问题的判读依据

现象执行计划特征根因修正方向
全表扫描 type=ALL,key=NULL,rows≈表总行数 过滤条件无法用于索引定位,如对索引列使用函数 改写条件为范围形式,或建立函数索引/生成列索引
索引失效 possible_keys 有值但 key=NULL 隐式类型转换、前导通配符、OR 连接非索引列 统一字段类型、去掉前导 %、拆分 OR 或补索引
回表过多 type=ref 或 range,rows 大,Extra 无 Using index 二级索引命中行数多,需逐行回聚簇索引取列 使用覆盖索引,或调整索引列顺序减少回表
EXPLAIN 访问路径判读 慢查询 SQL WHERE DATE(created_at)=… EXPLAIN 输出 type / key / rows / Extra 判读入口:type 字段 决定后续分支走向 type = ALL → 全表扫描 possible_keys 有值但 key=NULL → 索引失效 type=ref/range 且 rows 大 → 回表过多 根因:索引列被函数包裹,无法定位 → 改写为范围条件 根因:隐式转换 / 前导通配符 / OR 非索引列 → 统一类型或补索引 根因:二级索引命中行多,需回聚簇索引 → 覆盖索引或调整列序 判读口诀 ① type=ALL 且 key=NULL → 全表扫描,优先检查条件是否可索引化 ② possible_keys 有值但 key=NULL → 索引失效,检查类型转换与通配符 ③ rows 大且 Extra 无 Using index → 回表过多,考虑覆盖索引

改写验证:从全表扫描到范围扫描

把 DATE(created_at) = '2024-05-01' 改写为左闭右开区间,让 created_at 恢复为可索引的比较条件。若已有 idx_created_at,优化器即可选择范围扫描。

SELECT id, user_id, amount, created_at
FROM orders
WHERE created_at >= '2024-05-01 00:00:00'
  AND created_at <  '2024-05-02 00:00:00'
  AND status = 1;

-- 预期执行计划变化(示例):
-- type: ALL → range
-- key:  NULL → idx_created_at
-- rows: 986420 → 约 3200
-- Extra: Using where → Using index condition
验证时不要只看 type 是否变好,还要对比 rows 估算值与 Extra 是否出现 Using index。若 type=range 但 rows 仍接近全表,说明范围过大或索引选择性不足,需要进一步收窄条件或调整索引列顺序。
EXPLAIN 的 rows 是优化器基于统计信息的估算值,不是实际扫描行数。判断回表是否过多时,应结合 EXPLAIN ANALYZE(MySQL 8.0.18+)或 SHOW STATUS LIKE 'Handler_read%' 观察真实读行数,避免仅凭估算值下结论。