MySQL EXPLAIN 从入门到实战完全指南
在日常开发中,我们经常会遇到 SQL 执行缓慢的问题。此时,MySQL EXPLAIN 便是排查性能瓶颈的“利器”——它能清晰展示 SQL 的执行计划,帮助我们判断索引是否生效、是否存在全表扫描、是否需要优化排序等。本文将从测试环境搭建开始,逐步拆解 EXPLAIN 的核心字段含义,并通过实战案例讲解如何利用它优化 SQL 性能,最后介绍 MySQL 8.0 带来的执行计划新特性。
一、准备:搭建测试环境
为了让大家更直观地理解 EXPLAIN 的用法,我们先创建一套测试数据。以下 SQL 可直接在 MySQL 中执行,用于生成数据库、表结构及模拟数据。
1.1 创建数据库与表
-- 1. 创建测试数据库 martin
CREATE DATABASE martin;
USE martin;
-- 2. 创建表 t1(含主键与普通索引)
DROP TABLE IF EXISTS t1;
CREATE TABLE `t1` (
`id` int NOT NULL AUTO_INCREMENT,
`a` int DEFAULT NULL,
`b` int DEFAULT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`), -- 主键索引
KEY `idx_a` (`a`), -- 普通索引 a
KEY `idx_b` (`b`) -- 普通索引 b
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 创建存储过程,批量插入 1000 条数据
DROP PROCEDURE IF EXISTS insert_t1;
DELIMITER ;;
CREATE PROCEDURE insert_t1()
BEGIN
DECLARE i int;
SET i = 1;
WHILE (i <= 1000) DO
INSERT INTO t1(a, b) VALUES(i, i);
SET i = i + 1;
END WHILE;
END;;
DELIMITER ;
CALL insert_t1(); -- 调用存储过程插入数据
-- 4. 复制 t1 表结构与数据到 t2
DROP TABLE IF EXISTS t2;
CREATE TABLE t2 LIKE t1;
INSERT INTO t2 SELECT * FROM t1;1.2 初识 EXPLAIN
执行以下语句,即可查看 SQL 的执行计划:
EXPLAIN SELECT * FROM t1 WHERE b = 100;
执行后会返回一张包含 12 个字段的表格,这些字段便是我们分析 SQL 性能的核心依据。接下来,我们逐一拆解这些字段的含义。
二、EXPLAIN 核心字段详解
EXPLAIN 结果包含 id、select_type、table、type 等 12 个字段,其中 select_type、type、key、key_len、Extra 是需要重点关注的核心字段(下文已标粗)。
| 列名 | 解释 |
|---|---|
| id | 查询编号,用于标识多表关联或子查询中的执行顺序 |
| select_type | 查询类型,区分简单查询、子查询、联合查询等 |
| table | 执行查询涉及的表 |
| partitions | 匹配的分区(仅分区表有效,非分区表为 NULL) |
| type | 表连接/查询类型,直接反映查询性能(从优到差排序) |
| possible_keys | MySQL 认为可能用到的索引(仅供参考,不一定实际使用) |
| key | 实际使用的索引(若为 NULL,说明未使用索引) |
| key_len | 实际使用的索引长度(可用于判断联合索引的生效列数) |
| ref | 与索引比较的列或常量(如 const 表示与常量比较) |
| rows | 预估需要扫描的行数(InnoDB 为估值,非精确值) |
| filtered | 按条件筛选后的行百分比(值越高,筛选效果越好) |
| Extra | 附加信息,包含性能优化的关键提示(如是否全表扫描、是否使用临时表等) |
2.1 select_type:区分查询类型
select_type 用于标识查询的复杂程度,常见值如下:
| select_type 值 | 解释 |
|---|---|
| SIMPLE | 简单查询(无关联、无子查询),最常见的类型 |
| PRIMARY | 复杂查询中的最外层查询(如子查询的外层、联合查询的第一个查询) |
| UNION | 联合查询中第二个及以后的查询(如 A UNION B 中的 B) |
| DEPENDENT UNION | 依赖外部查询结果的联合查询(外层查询的结果影响内层联合查询) |
| SUBQUERY | 子查询中的第一个查询(不依赖外层结果) |
| DEPENDENT SUBQUERY | 依赖外层查询结果的子查询(外层每行都需触发子查询执行) |
| DERIVED | 派生表查询(如 FROM 子句中的子查询,MySQL 会先将结果存入临时表) |
| MATERIALIZED | 物化子查询(MySQL 将子查询结果缓存为临时表,避免重复执行) |
示例:子查询的 select_type
EXPLAIN SELECT * FROM t1 WHERE a = (SELECT a FROM t2 WHERE id = 10);
此时,子查询 (SELECT a FROM t2 WHERE id = 10) 的 select_type 为 SUBQUERY,外层查询的 select_type 为 PRIMARY。
2.2 type:判断查询性能等级
type 是 EXPLAIN 中最核心的字段之一,它表示表的查询/连接方式,性能从优到差依次为:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
| type 值 | 解释 | 适用场景示例 |
|---|---|---|
| system | 表仅有 1 行数据(仅 MyISAM/Memory 引擎支持),性能最优 | 查询 MyISAM 引擎的单行情报表 |
| const | 基于主键/唯一索引的等值查询,仅返回 1 行结果 | SELECT * FROM t1 WHERE id = 100 |
| eq_ref | 多表关联时,基于主键/唯一索引的等值匹配(每行仅匹配 1 行) | SELECT * FROM t1 JOIN t2 ON t1.id = t2.id |
| ref | 基于普通索引的等值查询,可能返回多行 | SELECT * FROM t1 WHERE b = 100(b 有普通索引) |
| range | 基于索引的范围查询(如 >、<、BETWEEN、IN) | SELECT * FROM t1 WHERE a BETWEEN 100 AND 200 |
| index | 全索引扫描(比全表扫描快,因索引文件更小) | SELECT a FROM t1(a 有索引,仅扫描索引树) |
| ALL | 全表扫描(性能最差,需避免) | SELECT * FROM t1 WHERE create_time = '2024-01-01'(create_time 无索引) |
优化建议:实际开发中,应尽量保证 type 至少达到 range 级别,避免 index 或 ALL(全表/全索引扫描)。
2.3 key_len:判断索引生效长度
key_len 表示实际使用的索引字节数,可用于判断 联合索引的生效列数(需结合字段类型计算)。常见字段类型的 key_len 计算规则如下:
| 列类型 | key_len 计算方式 | 备注 |
|---|---|---|
| int(允许 NULL) | 4 + 1 = 5 字节 | int 占 4 字节,NULL 需 1 字节标记 |
| int(NOT NULL) | 4 字节 | 无 NULL 标记,仅字段本身长度 |
| bigint(允许 NULL) | 8 + 1 = 9 字节 | bigint 占 8 字节 |
| char(30) utf8(NULL) | 30 * 3 + 1 = 91 字节 | utf8 中 1 字符占 3 字节,char 是定长类型 |
| varchar(30) utf8(NULL) | 30 * 3 + 2 + 1 = 93 字节 | varchar 是变长类型,需 2 字节存储长度,NULL 需 1 字节标记 |
| datetime(MySQL 8.0) | 5 + 1 = 6 字节 | 5.6.4 后 datetime 优化为 5 字节存储,NULL 需 1 字节 |
示例:联合索引 idx_a_b(a, b),若查询 WHERE a = 100,key_len 为 5(int 允许 NULL);若查询 WHERE a = 100 AND b = 200,key_len 为 5 + 5 = 10 字节,说明联合索引的两列均生效。
2.4 Extra:性能优化的关键提示
Extra 字段包含大量优化相关的细节,常见值及优化建议如下:
| Extra 值 | 解释 | 优化建议 |
|---|---|---|
| Using filesort | 非索引排序(需在内存/磁盘排序,性能差) | 给排序字段添加索引(如 ORDER BY create_time 需 idx_create_time) |
| Using temporary | 创建临时表存储中间结果(常见于无索引的 GROUP BY) | 给 GROUP BY 字段添加索引 |
| Using index | 覆盖索引(仅扫描索引即可获取结果,无需回表,性能优) | 保持查询字段在索引中(如 SELECT a FROM t1 用 idx_a) |
| Using where | 需通过 WHERE 筛选结果(若结合全表扫描,需优化) | 给 WHERE 条件字段添加索引 |
| Impossible WHERE | WHERE 条件恒为 false(如 1 < 0),无数据返回 | 检查条件逻辑,避免无效查询 |
| Using join buffer | 关联查询中,被驱动表无索引,需用连接缓冲区(性能差) | 给被驱动表的关联字段添加索引 |
| Select tables optimized away | 用聚合函数(max/min)访问索引字段,MySQL 直接优化为索引查找 | 无需优化,已是最优状态 |
示例:Using filesort 优化前:
-- 无索引的 ORDER BY,出现 Using filesort EXPLAIN SELECT * FROM t1 ORDER BY create_time;
优化后(添加索引):
ALTER TABLE t1 ADD INDEX idx_create_time(create_time); EXPLAIN SELECT * FROM t1 ORDER BY create_time; -- 无 Using filesort
三、实战:EXPLAIN 优化案例
理论结合实践才能更好地掌握 EXPLAIN,以下通过 3 个实战案例,展示如何用 EXPLAIN 定位并解决性能问题。
3.1 案例 1:对比有无索引的执行计划
索引是优化 SQL 的核心手段,我们通过 EXPLAIN 对比“主键查询”“有索引查询”“无索引查询”的差异:
| 查询场景 | SQL 语句 | type 类型 | key 索引 | 性能结论 |
|---|---|---|---|---|
| 主键查询(有唯一索引) | EXPLAIN SELECT * FROM t1 WHERE id = 100 | const | PRIMARY | 最优,仅扫描 1 行 |
| 普通索引查询 | EXPLAIN SELECT * FROM t1 WHERE b = 100 | ref | idx_b | 优秀,扫描少量行 |
| 无索引查询(删除 idx_b) | ALTER TABLE t1 DROP INDEX idx_b;EXPLAIN SELECT * FROM t1 WHERE b = 100 | ALL | NULL | 最差,全表扫描 1000 行 |
结论:索引能显著降低扫描行数,将 type 从 ALL(全表)提升至 ref 或 const。

3.2 案例 2:分析分区表的执行计划
对于海量数据,分区表是常用方案。EXPLAIN 的 partitions 字段可展示查询命中的分区,帮助验证分区有效性。
步骤 1:创建分区表
CREATE TABLE sales ( sale_id INT, sale_date DATE, amount DECIMAL(10, 2) ) PARTITION BY RANGE (YEAR(sale_date)) ( -- 按年份分区 PARTITION p2022 VALUES LESS THAN (2023), PARTITION p2023 VALUES LESS THAN (2024), PARTITION p2024 VALUES LESS THAN MAXVALUE ); -- 插入 2022-2024 年数据 INSERT INTO sales (sale_id, sale_date, amount) VALUES (1, '2022-01-01', 100.50), (8, '2024-08-23', 270.60), (10, '2024-10-05', 280.75);
步骤 2:查看分区执行计划
-- 查询 2024 年数据,仅命中 p2024 分区 EXPLAIN SELECT * FROM sales WHERE sale_date = '2024-08-23'; -- 查询 2023 年后数据,命中 p2023、p2024 分区 EXPLAIN SELECT * FROM sales WHERE sale_date > '2023-01-01';
此时 partitions 字段会显示 p2024 或 p2023,p2024,说明分区生效,避免了全分区扫描。

3.3 案例 3:排查正在执行的慢查询
当生产环境出现慢查询时,可通过 EXPLAIN FOR CONNECTION 查看其执行计划,无需等待查询结束。
步骤 1:构造慢查询
在窗口 1 执行一条含 sleep(100) 的慢查询:
SELECT *, SLEEP(100) FROM t1 LIMIT 1; -- 执行时间约 100 秒

步骤 2:获取连接 ID
在窗口 2 执行 show processlist,找到慢查询的 Id(如 12):
show processlist;
步骤 3:查看慢查询执行计划
EXPLAIN FOR CONNECTION 12; -- 12 为慢查询的 Id
通过此方式,可快速定位慢查询是否存在全表扫描、未使用索引等问题,及时优化。

四、MySQL 8.0 执行计划新特性
MySQL 8.0 对 EXPLAIN 进行了增强,新增了 树状执行计划 和 EXPLAIN ANALYZE,进一步提升了优化效率。
4.1 树状执行计划(format=tree)
从 MySQL 8.0.16 开始,支持输出树状结构的执行计划,更直观地展示查询逻辑(如关联顺序、过滤条件)。
EXPLAIN FORMAT=TREE SELECT * FROM t1 WHERE a = 100;
执行结果如下(结构清晰,包含预估成本和行数):
-> Rows fetched before execution (cost=0.25 rows=1)
-> Index lookup on t1 using idx_a (a=100) (cost=0.25 rows=1)

4.2 EXPLAIN ANALYZE(实际执行分析)
从 MySQL 8.0.18 开始,EXPLAIN ANALYZE 会实际执行 SQL,并返回更精确的执行信息(如实际扫描行数、执行时间、循环次数),解决了传统 EXPLAIN 估值不准的问题。
EXPLAIN ANALYZE SELECT * FROM t1 WHERE a BETWEEN 100 AND 200;
执行结果包含以下关键信息:
actual time: 实际执行时间(如0.02秒)actual rows: 实际扫描行数(如101行)loops: 循环次数(如1次)
注意:EXPLAIN ANALYZE 会执行 SQL,若为写操作(如 INSERT/UPDATE),需先备份数据或在测试环境使用。

五、总结
EXPLAIN 是 MySQL 性能优化的“基石”,掌握它的核心要点可帮助我们快速定位问题:
- 重点关注字段:
type(性能等级)、key(实际索引)、Extra(优化提示); - 索引优化原则:避免
type=ALL(全表扫描),消除Using filesort和Using temporary; - 实战技巧:用
EXPLAIN FOR CONNECTION排查慢查询,用 MySQL 8.0 的EXPLAIN ANALYZE获取精确执行信息。
希望本文能帮助你更好地利用 EXPLAIN 优化 SQL 性能,让数据库查询更高效!
到此这篇关于MySQL EXPLAIN 从入门到实战完全指南的文章就介绍到这了,更多相关mysql explain实战内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
mysql8.0.43使用InnoDB Cluster配置主从复制
本文主要介绍了mysql8.0.43使用InnoDB Cluster配置主从复制,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2025-09-09


最新评论