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 子句的子查询)
UNIONUNION 查询
UNION RESULTUNION 结果
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 filesortUsing 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

拿到一个执行计划,按这个顺序检查:

  1. type 字段:是否达到 ref 以上?ALL 需要优化
  2. key 字段:是否用到了索引?没用到看 possible_keys
  3. rows 字段:扫描行数是否合理?太大需要优化
  4. Extra 字段
    • Using filesort:需要加索引消除
    • Using temporary:需要加索引消除
    • Using where:确认是否可以通过索引过滤
    • Using index:覆盖索引,最理想

总结

EXPLAIN 是 SQL 优化的第一步:

  • type:从 const/eq_ref/ref/range/index/ALL 判断查找方式,越靠前越好
  • key:实际使用的索引,为 NULL 说明没用索引
  • rows:扫描行数,越小越好
  • Extra:额外信息,Using filesortUsing temporary 是常见问题

优化 SQL 的套路:

  1. EXPLAIN 看执行计划
  2. type=ALLrows 很大、Using filesort 的问题
  3. 加索引、改 SQL 结构
  4. EXPLAIN 验证效果

学会读懂执行计划,SQL 优化就学会了一半。

最后更新 4/20/2026, 4:48:48 AM