Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > mysql explain实战

MySQL EXPLAIN 从入门到实战完全指南

作者:·云扬·

本文详细介绍了MySQL的EXPLAIN工具,该工具可以帮助开发者理解SQL查询的执行计划,从而优化查询性能,本文结合实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧

在日常开发中,我们经常会遇到 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 结果包含 idselect_typetabletype 等 12 个字段,其中 select_typetypekeykey_lenExtra 是需要重点关注的核心字段(下文已标粗)。

列名解释
id查询编号,用于标识多表关联或子查询中的执行顺序
select_type查询类型,区分简单查询、子查询、联合查询等
table执行查询涉及的表
partitions匹配的分区(仅分区表有效,非分区表为 NULL)
type表连接/查询类型,直接反映查询性能(从优到差排序)
possible_keysMySQL 认为可能用到的索引(仅供参考,不一定实际使用)
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_typeSUBQUERY,外层查询的 select_typePRIMARY

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基于索引的范围查询(如 ><BETWEENINSELECT * 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 级别,避免 indexALL(全表/全索引扫描)。

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 = 100key_len 为 5(int 允许 NULL);若查询 WHERE a = 100 AND b = 200key_len 为 5 + 5 = 10 字节,说明联合索引的两列均生效。

2.4 Extra:性能优化的关键提示

Extra 字段包含大量优化相关的细节,常见值及优化建议如下:

Extra 值解释优化建议
Using filesort非索引排序(需在内存/磁盘排序,性能差)给排序字段添加索引(如 ORDER BY create_timeidx_create_time
Using temporary创建临时表存储中间结果(常见于无索引的 GROUP BY)给 GROUP BY 字段添加索引
Using index覆盖索引(仅扫描索引即可获取结果,无需回表,性能优)保持查询字段在索引中(如 SELECT a FROM t1idx_a
Using where需通过 WHERE 筛选结果(若结合全表扫描,需优化)给 WHERE 条件字段添加索引
Impossible WHEREWHERE 条件恒为 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 = 100constPRIMARY最优,仅扫描 1 行
普通索引查询EXPLAIN SELECT * FROM t1 WHERE b = 100refidx_b优秀,扫描少量行
无索引查询(删除 idx_b)ALTER TABLE t1 DROP INDEX idx_b;
EXPLAIN SELECT * FROM t1 WHERE b = 100
ALLNULL最差,全表扫描 1000 行

结论:索引能显著降低扫描行数,将 typeALL(全表)提升至 refconst

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 字段会显示 p2024p2023,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;

执行结果包含以下关键信息:

注意EXPLAIN ANALYZE 会执行 SQL,若为写操作(如 INSERT/UPDATE),需先备份数据或在测试环境使用。

五、总结

EXPLAIN 是 MySQL 性能优化的“基石”,掌握它的核心要点可帮助我们快速定位问题:

  1. 重点关注字段type(性能等级)、key(实际索引)、Extra(优化提示);
  2. 索引优化原则:避免 type=ALL(全表扫描),消除 Using filesortUsing temporary
  3. 实战技巧:用 EXPLAIN FOR CONNECTION 排查慢查询,用 MySQL 8.0 的 EXPLAIN ANALYZE 获取精确执行信息。

希望本文能帮助你更好地利用 EXPLAIN 优化 SQL 性能,让数据库查询更高效!

到此这篇关于MySQL EXPLAIN 从入门到实战完全指南的文章就介绍到这了,更多相关mysql explain实战内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文