EXPLAIN 不是"看一眼 type"
慢查询优化时,很多人打开 EXPLAIN,看到 type=ALL 就加索引,看到 type=ref 就认为没问题。结果索引加了一堆,查询还是慢;或者明明走了索引,扫描行数却大得离谱。
EXPLAIN 输出的是一份"优化器打算怎么执行"的报告,里面十来个字段要连起来看:用哪个索引、索引用了多长、预估扫多少行、过滤比例多少、有没有额外排序。只看 type 很容易误判。
关键列逐个说
| 列 | 含义 | 怎么看 |
|---|---|---|
id |
查询序号 | 数字越大越先执行,相同则从上往下 |
select_type |
查询类型 | SIMPLE、PRIMARY、SUBQUERY、DERIVED、UNION |
table |
访问的表 | 可能是派生表名 |
partitions |
命中的分区 | 分区表才关注 |
type |
访问类型 | 从好到差:system > const > eq_ref > ref > range > index > ALL |
possible_keys |
可能用到的索引 | 只是候选,不代表实际用 |
key |
实际使用的索引 | 为 NULL 表示没走索引 |
key_len |
索引使用长度 | 判断联合索引用到了第几列 |
ref |
索引比较的对象 | 常量、列名或函数结果 |
rows |
预估扫描行数 | 估算值,不代表最终返回行数 |
filtered |
过滤后剩余百分比 | 越小说明过滤效果越好 |
Extra |
附加信息 | 排序、临时表、覆盖索引都在这里 |
key_len 是判断联合索引的利器。以 (user_id, status, create_time) 为例,如果 user_id 是 bigint(8 字节)、status 是 tinyint(1 字节)、create_time 是 datetime(5 字节),那么:
- 只用到
user_id:key_len约 8; - 用到
user_id + status:约 9; - 三列全用上:约 14。
再考虑列是否允许 NULL,key_len 会多 1 字节。对照表结构算一下,就能知道索引到底用到了哪一列。
type 与 Extra 怎么读
type 反映访问方式,常用的几档:
const:主键或唯一索引等值查询,最多一行;eq_ref:联表时被驱动表用主键/唯一索引匹配,每行只取一条;ref:普通索引等值查询,可能多行;range:索引范围扫描,between、>、in常见;index:扫描整棵索引树,比ALL好一点,但仍然是全量;ALL:全表扫描。
Extra 里几个高频词:
| 值 | 含义 | 是否要处理 |
|---|---|---|
Using index |
覆盖索引,不回表 | 好事 |
Using where |
存储引擎返回后在 Server 层过滤 | 正常,结合 rows 看 |
Using index condition |
索引下推,在引擎层过滤 | 通常是好事 |
Using filesort |
需要额外排序 | 数据量大时要优化 |
Using temporary |
用了临时表 | group by、distinct 常见,要关注 |
Using join buffer |
联表没有合适索引 | 通常需要加索引 |
一个实际例子:
EXPLAIN
SELECT id, amount
FROM t_order
WHERE user_id = 100 AND status = 1
ORDER BY create_time DESC;
如果 key 是 idx_user_status_time、rows 只有几十、Extra 里没有 Using filesort,说明索引顺序和查询、排序都匹配,这条 SQL 基本不用再改。
MySQL 8.0.18 以后还可以用 EXPLAIN ANALYZE,它会真正执行语句并给出实际耗时和行数:
EXPLAIN ANALYZE
SELECT id, amount FROM t_order WHERE user_id = 100;
注意它是真的执行,不是估算。对 UPDATE、DELETE 用之前,先包在事务里并准备回滚。
常见坑
1. 只看 type 不看 rows
type=ref 不代表快。如果 rows 是几百万,照样慢。反过来,小表上 type=ALL 可能比走索引还快,优化器选择全表扫描是有成本依据的。
2. 把 possible_keys 当成实际索引
possible_keys 只是候选,key 才是实际用的。两个都有值时,要看 key 具体是哪一个,别优化错了对象。
3. 测试库数据量太小
开发库只有几千行,怎么查都是全表扫描最快,执行计划和生产完全不是一回事。优化要在接近真实数据量的环境里做,至少要看 rows 的数量级。
4. 统计信息过期
rows 是估算值,依赖统计信息。表数据变化很大时,先执行 ANALYZE TABLE t_order,再看执行计划。否则可能看到"明明有索引却不走"的假象。
5. 绑定变量导致计划不同
同样的 SQL,参数不同,优化器可能选择不同索引。用 EXPLAIN 时要带入真实参数,不要用 ? 占位后随便填一个值。
6. 对 DML 直接跑 EXPLAIN ANALYZE
EXPLAIN ANALYZE UPDATE ... 会真的改数据。要么在事务里执行后回滚,要么只对 SELECT 使用。生产环境更要谨慎。
7. 只优化一条 SQL
单条 SQL 从 2 秒降到 200 毫秒,但如果 QPS 很高、并发一上来,数据库 CPU 和连接数仍然是瓶颈。优化要结合整体并发和资源水位看。
排查步骤
比较稳妥的流程是:
- 从慢查询日志里捞出耗时最长的 SQL,确认执行频率;
- 用真实参数跑
EXPLAIN,记录type、key、key_len、rows、Extra; - 判断问题类型:没走索引、索引用了但回表多、排序产生 filesort、临时表过大;
- 针对性调整:加联合索引、改 SQL 写法、减少
select *、拆分大查询; - 调整后再
EXPLAIN对比,必要时用EXPLAIN ANALYZE看实际行数; - 上线后观察慢查询数量和接口耗时,确认没有把压力转移到写入侧。
执行计划是手段,不是目的。最终要回答的问题是:这条 SQL 扫了多少行、回了多少次表、排序和临时表有多大。把这几个数字降下来,查询自然就快了。
评论