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 |
二级索引命中行数多,需逐行回聚簇索引取列 | 使用覆盖索引,或调整索引列顺序减少回表 |
改写验证:从全表扫描到范围扫描
把 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%' 观察真实读行数,避免仅凭估算值下结论。