MySQL EXPLAIN 深度解析,手把手教你读懂执行计划
MySQL EXPLAIN 深度解析,手把手教你读懂执行计划
优化 SQL 的第一步永远是 EXPLAIN,但很多同学只会看个 type 字段,看到 ALL 就知道要加索引,至于为什么、怎么优化就不清楚了。
今天把 EXPLAIN 的每个字段彻底讲清楚。
EXPLAIN 基础用法
EXPLAIN SELECT * FROM orders WHERE user_id = 10086;
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE user_id = 10086; -- JSON 格式,详细信息
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 10086; -- MySQL 8.0+,实际执行并计时
EXPLAIN ANALYZE 是 MySQL 8.0 加入的功能,可以看实际执行时间和预估时间的差异,非常有用。
各字段详解
id - 查询编号
EXPLAIN SELECT * FROM orders WHERE user_id = (
SELECT user_id FROM users WHERE id = 10086
);
+----+-------------+--------+...
| id | select_type | table |...
+----+-------------+--------+...
| 1 | PRIMARY | orders |
| 2 | SUBQUERY | users |
+----+-------------+--------+...
id 越大越先执行,id 相同则由上到下执行。
select_type - 查询类型
| 值 | 含义 |
|---|---|
| SIMPLE | 简单 SELECT,不含子查询或 UNION |
| PRIMARY | 最外层 SELECT |
| SUBQUERY | 子查询 |
| DERIVED | 派生表(FROM 子句的子查询) |
| UNION | UNION 查询 |
| UNION RESULT | UNION 结果 |
EXPLAIN SELECT * FROM orders
UNION
SELECT * FROM orders_archive;
+----+--------------+------------+...
| id | select_type | table |...
+----+--------------+------------+...
| 1 | PRIMARY | orders |
| 2 | UNION | orders_archive |
| 3 | UNION RESULT | <union1,2> |
+----+--------------+------------+...
table - 表名
显示这条数据属于哪张表,如果是子查询或 UNION,会显示临时表名如 <union1,2> 或 <derived3>。
type - 访问类型(重要)
type 描述了 MySQL 决定如何查找数据,是判断 SQL 性能的最重要指标之一。
从好到差排序:
| type | 含义 | 说明 |
|---|---|---|
| system | 系统表,只有 1 条记录 | 最好 |
| const | 常量查找,主键或唯一索引等值查询 | 极好 |
| eq_ref | 唯一索引等值扫描,对于每个索引键只返回一行 | 很好 |
| ref | 非唯一索引等值扫描,返回匹配某值的所有行 | 好 |
| range | 索引范围扫描, BETWEEN、IN、>、>= 等 | 可接受 |
| index | 全索引扫描,按索引顺序遍历 | 较差 |
| ALL | 全表扫描 | 最差 |
-- system
EXPLAIN SELECT * FROM mysql.time_zone_name LIMIT 1;
-- const:主键等值查询
EXPLAIN SELECT * FROM orders WHERE id = 1;
-- eq_ref:连接时使用唯一索引
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- ref:非唯一索引等值查询
EXPLAIN SELECT * FROM orders WHERE user_id = 10086;
-- range:范围查询
EXPLAIN SELECT * FROM orders WHERE created_at >= '2026-01-01';
-- index:全索引扫描
EXPLAIN SELECT COUNT(*) FROM orders;
-- ALL:全表扫描
EXPLAIN SELECT * FROM orders WHERE status = 1; -- status 无索引
优化目标:至少达到 ref 级别,尽量避免 ALL。
possible_keys - 可能使用的索引
显示查询可能用到的索引,但不一定真的使用。
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = 1;
+-------------+-------------+...
| possible_keys | key |
+-------------+-------------+...
| idx_user_id | idx_user_id |
+-------------+-------------+...
possible_keys 有值但 key 为 NULL,说明有索引但 MySQL 优化器没选。
key - 实际使用的索引
显示实际使用的索引。如果为 NULL,说明没有用到索引。
EXPLAIN SELECT * FROM orders WHERE status = 1; -- status 无索引
+-------------+------+
| possible_keys | key |
+-------------+------+
| NULL | NULL |
+-------------+------+
key_len - 索引长度
显示使用的索引长度,可以用来判断复合索引使用了多少列。
CREATE TABLE test (
a INT,
b INT,
c INT,
KEY idx_abc (a, b, c)
);
EXPLAIN SELECT * FROM test WHERE a = 1 AND b = 2;
+--------+--------+...
| key_len | Extra |
+--------+--------+...
| 10 | ... |
+--------+--------+...
key_len = 10 意味着使用了索引的前两列(每列 INT 占 4 字节,还有 2 字节可能是 NULL 标记)。
rows - 预估扫描行数
MySQL 估算的要读取和检查的行数,不是返回行数。
EXPLAIN SELECT * FROM orders WHERE user_id = 10086;
+------+--------+...
| rows | Extra |
+------+--------+...
| 156 | ... |
+------+--------+...
rows = 156 意味着 MySQL 估算要检查 156 行才能找到结果。
优化目标:rows 越小越好。如果 rows 很大但实际返回很少,说明需要更好的索引。
filtered - 过滤比例
MySQL 8.0 加入,表示过滤后剩余的百分比。
rows = 1000
filtered = 10
意味着估算要检查 1000 行,其中 10%(100 行)会通过 WHERE 条件。
Extra - 额外信息(重要)
这个字段包含了很多重要信息:
Using index - 覆盖索引
EXPLAIN SELECT user_id, status FROM orders WHERE user_id = 10086;
+------+--------+...
| Extra | Using index |
+------+--------+...
| NULL | Using index |
+------+--------+...
Using index 表示只通过索引就能获取到所有需要的数据,不需要回表,性能很好。
Using where - 需要额外过滤
EXPLAIN SELECT * FROM orders WHERE status = 1 AND user_id = 10086;
+------+--------+...
| Extra | Using where |
+------+--------+...
| NULL | Using where |
+------+--------+...
Using where 表示 MySQL 在存储引擎返回数据后,用 WHERE 条件做过滤。
Using index condition - 索引条件下推
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status LIKE '1%';
+------+--------+...
| Extra | Using index condition |
+------+--------+...
| NULL | Using index condition |
+------+--------+...
Using index condition 表示先在索引层面过滤一部分数据,减少回表次数。
Using filesort - 需要额外排序
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 ORDER BY created_at DESC;
+------+--------+...
| Extra | Using filesort |
+------+--------+...
| NULL | Using filesort |
+------+--------+...
Using filesort 表示 MySQL 无法利用索引顺序排序,需要额外的排序操作(可能在内存或磁盘)。
优化:尽量让 ORDER BY 命中索引,避免 filesort。
Using temporary - 使用临时表
EXPLAIN SELECT * FROM orders GROUP BY user_id;
+------+--------+...
| Extra | Using temporary |
+------+--------+...
| NULL | Using temporary |
+------+--------+...
Using temporary 表示需要创建临时表来存储结果,通常和 filesort 一起出现。
优化:加合适的索引消除临时表和排序。
Using join buffer - 连接使用缓存
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
+------+--------+...
| Extra | Using join buffer |
+------+--------+...
| NULL | Using join buffer |
+------+--------+...
Using join buffer 表示连接时使用了 join buffer,说明被连接表没有索引,需要全表扫描 + 缓存。
Not exists - NOT EXISTS 查询
EXPLAIN SELECT * FROM orders o LEFT JOIN order_items oi ON o.id = oi.order_id WHERE oi.id IS NULL;
+------+--------+...
| Extra | Not exists |
+------+--------+...
| NULL | Not exists |
+------+--------+...
Not exists 表示优化了 LEFT JOIN 的 NOT NULL 检查。
实战分析
案例一:简单的等值查询
EXPLAIN SELECT * FROM orders WHERE order_no = 'AK20260228001';
+----+-------------+--------+------------+-------+---------------+--------+---------+-------+------+----------+-------+
| id | select_type | table | type | key | key_len | rows | filtered| Extra |
+----+-------------+--------+------------+-------+---------------+--------+---------+-------+------+----------+-------+
| 1 | SIMPLE | orders | const | PRIMARY| 8 | 1 | 100.00| NULL |
+----+-------------+--------+------------+-------+---------------+--------+---------+-------+------+----------+-------+
分析:
type = const:主键常量查询,命中主键索引,只查 1 行key = PRIMARY:使用主键索引rows = 1:只检查 1 行
完美。
案例二:多表 JOIN
EXPLAIN SELECT o.*, u.username
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.user_id = 10086;
+----+-------------+-------+--------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | type | key | key_len | rows | Extra |
+----+-------------+-------+--------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | o | ref | idx_user_id | 8 | 50 | NULL |
| 1 | SIMPLE | u | eq_ref | PRIMARY | 8 | 1 | NULL |
+----+-------------+-------+--------+---------------+---------+---------+-------+------+----------+-------+
分析:
o表:type = ref,使用idx_user_id索引,估算 50 行u表:type = eq_ref,主键连接,每行返回 1 行- 都没有
Using filesort或Using temporary
性能良好。
案例三:需要优化的查询
EXPLAIN SELECT * FROM orders
WHERE status = 1
AND created_at >= '2026-01-01'
ORDER BY created_at DESC
LIMIT 20;
+----+-------------+--------+-------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table | type | key | ... | rows | ... | Extra |
+----+-------------+--------+-------+---------------+------+---------+------+--------+----------+-------------+
| 1 | SIMPLE | orders | range | idx_status_dt | ... | 9865432 | ... | Using index condition; Using filesort |
+----+-------------+--------+-------+---------------+------+---------+------+--------+----------+-------------+
问题分析:
type = range:范围查询,可以接受rows = 9865432:扫描近千万行,只为返回 20 条Using filesort:无法利用索引排序Using index condition:有索引下推优化
优化方案:加复合索引 (status, created_at)
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);
EXPLAIN SELECT * FROM orders
WHERE status = 1
AND created_at >= '2026-01-01'
ORDER BY created_at DESC
LIMIT 20;
+----+-------------+--------+-------+------------------+------+---------+------+----------+-------+
| id | select_type | table | type | key | ... | rows | ... | Extra |
+----+-------------+--------+-------+------------------+------+---------+------+----------+-------+
| 1 | SIMPLE | orders | ref | idx_status_created | ... | 156 | ... | Using index condition |
+----+-------------+--------+-------+------------------+------+---------+------+----------+-------+
优化后:
rows = 156:从近千万降到 156- 无
Using filesort:利用了索引有序特性 - 性能提升 99%
EXPLAIN 优化 Checklist
拿到一个执行计划,按这个顺序检查:
- type 字段:是否达到 ref 以上?ALL 需要优化
- key 字段:是否用到了索引?没用到看 possible_keys
- rows 字段:扫描行数是否合理?太大需要优化
- Extra 字段:
Using filesort:需要加索引消除Using temporary:需要加索引消除Using where:确认是否可以通过索引过滤Using index:覆盖索引,最理想
总结
EXPLAIN 是 SQL 优化的第一步:
type:从 const/eq_ref/ref/range/index/ALL 判断查找方式,越靠前越好key:实际使用的索引,为 NULL 说明没用索引rows:扫描行数,越小越好Extra:额外信息,Using filesort和Using temporary是常见问题
优化 SQL 的套路:
EXPLAIN看执行计划- 找
type=ALL、rows很大、Using filesort的问题 - 加索引、改 SQL 结构
- 再
EXPLAIN验证效果
学会读懂执行计划,SQL 优化就学会了一半。
