
你找到了慢查询日志,发现一条SQL执行了5秒。你给它加了索引,还是慢。你又加了一个,还是慢。
你在猜。你不知道MySQL到底怎么执行这条SQL,是全表扫描还是用了索引?用了哪个索引?扫了多少行?在哪里花了时间?
EXPLAIN就是让你看这些的。今天把EXPLAIN的输出拆开讲,以后慢SQL在你眼里就是透明的。
先看一个数据
某运维团队统计,超过80%的SQL性能问题,通过EXPLAIN分析就能定位。不需要高级调优,不需要改架构。但前提是你会看EXPLAIN。
很多人只看了key列(用了哪个索引),没看type和Extra。索引用上了,但用得不高效,一样慢。
基础用法
sql
EXPLAIN SELECT * FROM orders WHERE user_id = 12345;
输出一张表,每一行是一个查询步骤(多表JOIN会有多行)。关键列:
| 列名 | 含义 | 重点关注 |
|---|---|---|
| id | 查询执行顺序 | JOIN时id相同,从上到下 |
| select_type | 查询类型 | SIMPLE简单查询,SUBQUERY子查询 |
| table | 表名 | — |
| type | 访问类型 | ALL最差,ref/range好,const/eq_ref最好 |
| possible_keys | 可能用的索引 | 有索引但没用上,说明写的有问题 |
| key | 实际用的索引 | NULL表示没走索引 |
| key_len | 索引长度 | 可判断复合索引用了哪几列 |
| rows | 预估扫描行数 | 越小越好 |
| Extra | 额外信息 | Using filesort、Using temporary是危险信号 |
重点看type、key、rows、Extra。
type列:访问类型(性能关键)
type从好到差排序:
| type | 含义 | 性能 | 示例 |
|---|---|---|---|
| system | 系统表,只有一行 | 最好 | 很少见 |
| const | 主键或唯一索引等值查询 | 极好 | WHERE id = 1 |
| eq_ref | JOIN时被驱动表用主键关联 | 很好 | JOIN ON t1.id = t2.id |
| ref | 非唯一索引等值查询 | 好 | WHERE user_id = 123(user_id有普通索引) |
| range | 索引范围扫描 | 中等 | WHERE id BETWEEN 1 AND 100 |
| index | 全索引扫描 | 较差 | 比ALL快一点,但仍是全扫 |
| ALL | 全表扫描 | 最差 | 必须优化 |
目标:让type至少达到range,最好达到ref或更高。看到ALL就是报警。
反常识点:有时index(全索引扫描)比range还慢。索引扫描顺序和存储顺序不一致时,大量离散I/O拖慢速度。
Extra列:额外信息(危险信号)
Extra里的几个关键词要特别留意:
| Extra信息 | 含义 | 解决方案 |
|---|---|---|
| Using filesort | 需要额外排序,没用索引 | 给ORDER BY的字段加索引 |
| Using temporary | 用了临时表(通常是GROUP BY或DISTINCT) | 优化查询,或加索引 |
| Using where | 用WHERE过滤 | 正常,但配合其他信息看 |
| Using index | 覆盖索引,不回表 | 好,说明索引已包含所有查询字段 |
| Using index condition | 索引下推 | 好,MySQL5.6+优化 |
| Using where + Using index | 索引列被WHERE使用,但没覆盖全部 | 可接受 |
重点:看到Using filesort或Using temporary,通常需要优化。这两项在数据量大时极其影响性能。
实战案例一:慢在filesort
sql
EXPLAIN SELECT * FROM users ORDER BY create_time LIMIT 10;
输出:
- type: ALL(全表扫描)
- Extra: Using filesort
问题:没有索引,MySQL要全表扫,再排序。给create_time加索引:
sql
ALTER TABLE users ADD INDEX idx_create_time (create_time);
再看EXPLAIN:
- type: index(用了索引)
- Extra: 无filesort
排序走索引,不再额外排序。百万级数据查询从2秒降到0.01秒。
实战案例二:复合索引顺序错误
sql
EXPLAIN SELECT * FROM orders WHERE status = 'paid' AND create_time > '2026-01-01';
有索引(create_time, status),但type是range,key_len只用了4字节(只用了create_time)。status没用到索引。
把索引顺序改成(status, create_time),因为status =等值查询放前面,create_time >范围查询放后面。
改后:
- type: range
- key_len用了更多字节,两个条件都用到索引
规则:复合索引中,等值查询放前面,范围查询放后面。
key_len:判断复合索引用了多少列
key_len表示索引使用的字节数。可以反推用了复合索引中的哪几列。
假设复合索引(status, create_time):
- status是VARCHAR(10) utf8mb4,一个字符4字节,加上长度标识,约40字节
- create_time是DATETIME,5字节
如果EXPLAIN显示key_len=40,说明只用了status列。如果key_len=45,说明两列都用了。
真实案例:一条慢查询的优化全过程
慢查询日志里有一条SQL执行了8秒:
sql
SELECT * FROM logs WHERE user_id = 12345 AND action = 'login' ORDER BY create_time DESC LIMIT 100;
EXPLAIN输出:
- type: ALL
- rows: 200万
- Extra: Using where; Using filesort
分析:全表扫描200万行,还要额外排序,数据量大,自然慢。
加复合索引:
sql
ALTER TABLE logs ADD INDEX (user_id, action, create_time);
再EXPLAIN:
- type: ref
- rows: 523
- Extra: 无filesort
查询时间从8秒降到0.02秒。一个索引解决所有问题。
其他常用技巧
查看MySQL是否用了索引下推:EXPLAIN输出Using index condition,说明索引条件下推优化已生效,对减少回表次数有帮助。
查看是否用了覆盖索引:Extra列显示Using index,且没有Using where,说明查询只访问索引,不回表,效率最高。
SHOW WARNINGS:EXPLAIN后执行SHOW WARNINGS,能看到MySQL重写后的SQL,有时能发现隐式类型转换等问题。
最后一句
EXPLAIN是SQL优化的眼睛。用EXPLAIN看执行计划,找到type=ALL,加索引。看到filesort,优化排序。看到rows很大,缩小范围。
下次你遇到慢SQL,不要猜。把EXPLAIN跑出来,看type,看rows,看Extra。三个地方看完,问题通常就找到了。优化后的SQL跑一遍,你会觉得以前的慢得不值。




