MySQL执行计划详解:从入门到实战,优化SQL不再难

在日常开发中,我们经常会遇到MySQL查询变慢的问题——明明逻辑没问题,数据量也不算特别大,但SQL执行就是“磨磨蹭蹭”。此时,MySQL执行计划(Explain) 就是我们排查问题的“手术刀”:它能清晰展示MySQL优化器对SQL的执行思路,比如是否使用索引、扫描多少行数据、是否需要临时表等,帮我们精准定位性能瓶颈,写出更高效的SQL。

今天,我们就从基础用法到进阶实战,全面拆解MySQL执行计划,让你彻底掌握这个核心优化工具。

一、Explain入门:是什么?怎么用?

1. 什么是Explain?

Explain是MySQL提供的内置工具,通过在SQL语句前添加EXPLAIN关键字,就能获取MySQL优化器对该SQL的执行计划预估——它不会实际执行SQL,但会告诉我们:

  • SQL是否使用了索引?用了哪个索引?
  • 表之间的连接方式是什么?
  • 预估需要扫描多少行数据?
  • 是否需要排序或临时表?

这些信息是优化SQL的核心依据,也是排查慢查询的第一步。

2. 基本用法

用法极其简单:在需要分析的SQL前直接加EXPLAIN即可。例如:

-- 分析“查询t1表中b=100”的执行计划
EXPLAIN SELECT * FROM t1 WHERE b=100;

执行后,MySQL会返回一张包含多个字段的结果表,每个字段都对应执行计划的关键信息。接下来,我们先搭建测试环境,再逐个拆解这些字段。

二、实战准备:搭建测试环境

为了让执行计划的分析更直观,我们先创建两张测试表(t1t2),并插入模拟数据。以下是完整的建表和数据插入语句,可直接复制执行:

-- 1. 创建数据库
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;

环境搭建完成后,我们就可以基于这两张表分析执行计划了。

三、核心解析:Explain结果的关键字段

执行EXPLAIN SELECT * FROM t1 WHERE b=100;后,会得到类似如下的结果(不同MySQL版本格式可能略有差异,但核心字段一致):
在这里插入图片描述

其中,加粗的字段是需要重点关注的核心项,我们逐个拆解:

列名解释
id查询编号
select_type查询类型:显示本行是简单还是复杂查询
table涉及到的表
partitions匹配的分区:查询将匹配记录所在的分区。仅当使用 partition 关键字时才显示该列。对于非分区表,该值为 NULL。
type本次查询的表连接类型
possible_keys可能选择的索引
key实际选择的索引
key_len被选择的索引长度:一般用于判断联合索引有多少列被选择了
ref与索引比较的列
rows预计需要扫描的行数,对 InnoDB 来说,这个值是估值,并不一定准确
filtered按条件筛选的行的百分比
Extra附加信息

1. select_type:查询类型

表示当前查询是简单查询还是复杂查询(如子查询、联合查询),常见值如下:

select_type值解释
SIMPLE简单查询(无关联、无子查询),最常见的类型
PRIMARY复杂查询中最外层的查询(如子查询的外层、联合查询的第一个查询)
UNION联合查询中第二个及以后的查询(如A UNION B中的B)
SUBQUERY子查询中第一个查询(如SELECT * FROM t1 WHERE id=(SELECT id FROM t2 where a=1)
DERIVED派生表查询(如SELECT * FROM (SELECT distinct id FROM t1) AS temp中的子查询)
MATERIALIZED被物化的子查询(MySQL会将子查询结果存储为临时表,避免重复计算)

重点:大部分日常查询都是SIMPLE类型,复杂查询(如SUBQUERYUNION)需要关注是否有性能损耗(如重复计算)。

2. type:表连接/扫描类型(性能核心!)

type字段直接反映了查询的性能优劣,它表示MySQL如何扫描表中的数据,从优到劣的排序如下(越靠上性能越好,日常优化至少要保证达到range级别,避免ALL):

type值解释适用场景
system表中只有1行数据(仅MyISAM/Memory引擎支持),最优情况(几乎遇不到)极小表的查询
const基于主键/唯一索引的等值查询,最多返回1行数据,性能极佳WHERE id=1(id是主键)
eq_ref关联查询中,被关联表基于主键/唯一索引扫描(每行只匹配1条)t1 JOIN t2 ON t1.id=t2.id(t2.id是主键)
ref基于普通索引的等值查询,可能返回多行(但性能仍较好)WHERE b=100(b有普通索引idx_b)
range基于索引的范围查询(如>、<、BETWEEN、INWHERE a BETWEEN 100 AND 200
index全索引扫描(扫描所有索引节点,但不扫描表数据),性能较差查询字段都在索引中,但无过滤条件
ALL全表扫描(扫描表中所有数据),性能最差,必须避免!WHERE create_time='2024-01-01'(无索引)

实战建议:如果typeALL(全表扫描),优先检查是否能加索引;如果是range,确认是否能用等值查询优化(如IN改等值)。

3. key & possible_keys:实际使用/可能使用的索引

  • possible_keys:MySQL优化器认为当前查询可能适用的索引(可能有多个,也可能为NULL);
  • key:MySQL优化器实际选择使用的索引(如果为NULL,说明没走索引)。

关键结论

  • 如果possible_keys有值但key为NULL,说明索引失效(如索引列用了函数、类型不匹配等);
  • 如果possible_keyskey都为NULL,说明没有合适的索引,需要考虑新增索引。

4. key_len:索引长度

表示当前查询中,MySQL实际使用的索引的字节长度,主要用于判断:

  1. 联合索引是否被完全利用(如联合索引idx_a_b(a,b),若key_len包含a和b的长度,说明联合索引被完全使用);
  2. 索引列是否允许NULL(允许NULL会多1字节)。

常见字段类型的key_len计算方式(以UTF8编码为例):

列类型KEY_LEN备注
intkey_len = 4+1int 为 4 bytes,允许为 NULL,加 1 byte
int not nullkey_len = 4不允许为 NULL
bigintkey_len=8+1bigint 为 8 bytes,允许为 NULL 加 1 byte
bigint not nullkey_len=8bigint 为 8 bytes
char(30) utf8key_len=30*3+1char(n)为: n * 3,允许为 NULL 加 1 byte
char(30) not null utf8key_len=30*3不允许为 NULL
varchar(30) not null utf8key_len=30*3+2utf8 每个字符为 3 bytes,变长数据类型,加 2 bytes
varchar(30) utf8key_len=30*3+2+1utf8 每个字符为 3 bytes,允许为 NULL,加 1 byte,变长数据类型,加 2 bytes
datetimekey_len=8+1 (MySQL 5.6.4之前的版本); key_len=5+1(MySQL 5.6.4及之后的版本)允许为 NULL,加 1 byte

实战用法:若联合索引idx_a_b(a,b)key_len仅包含a的长度(如5),说明b列未被利用,需检查查询条件是否包含b。

5. rows:预估扫描行数

MySQL优化器预估的、当前查询需要扫描的行数(InnoDB引擎下是估值,不一定完全准确,但可作为参考)。

核心逻辑rows越小,查询性能越好。如果rows远大于实际数据量,可能是MySQL统计信息过时,需执行ANALYZE TABLE 表名更新统计信息。

6. Extra:附加信息(性能问题的“预警灯”)

Extra字段包含MySQL执行SQL时的额外信息,很多时候是性能问题的直接信号,常见值如下:

Extra值解释性能影响例子
Using index使用覆盖索引(查询字段都在索引中,无需回表查数据),性能优秀正面(推荐)explain select a from t1 where a=111;
Using filesort无法用索引排序,需在内存/磁盘中做外部排序,性能差负面(需优化)explain select * from t1 order by create_time;
Using temporary需要创建临时表存储中间结果(如无索引的GROUP BY),性能差负面(需优化)explain select * from t1 group by create_time;
Using where需用WHERE条件过滤数据(正常情况,非性能问题)中性explain select * from t1 where create_time='2019-06-18 14:38:24';
Impossible WHEREWHERE条件恒为false(如WHERE 1<0),无数据返回中性explain select * from t1 where 1<0;
Using join buffer关联查询中,被驱动表无索引,需用缓冲区暂存数据,性能差负面(需优化)explain select * from t1 straight_join t2 on (t1.create_time=t2.create_time);

实战案例

  • 若出现Using filesort:检查排序字段是否有索引(如ORDER BY create_time需加idx_create_time);
  • 若出现Using temporary:检查GROUP BY的字段是否有索引(如GROUP BY a需加idx_a)。

四、对比实验:有无索引对执行计划的影响

光说不练假把式,我们通过“有无索引”的对比,直观感受索引对执行计划的影响:

实验1:有索引的情况(b字段有idx_b索引)

执行:

EXPLAIN SELECT * FROM t1 WHERE b=100;

关键结果:

  • type=ref(普通索引等值查询,性能好);
  • key=idx_b(实际走了索引);
  • rows=1(仅扫描1行)。

在这里插入图片描述

实验2:无索引的情况(删除b字段的idx_b索引)

先删除索引:

ALTER TABLE t1 DROP INDEX idx_b;

再执行同样的查询:

EXPLAIN SELECT * FROM t1 WHERE b=100;

关键结果:

  • type=ALL(全表扫描,性能差);
  • key=NULL(没走索引);
  • rows=1000(预估扫描所有1000行数据)。
    在这里插入图片描述

结论:索引能直接将typeALL(全表)优化为ref(索引),扫描行数从1000降到1,性能提升显著。

五、特殊场景:分区表与实时执行计划

除了常规查询,Explain还能应对一些特殊场景,比如分区表和线上实时慢查询。

1. 分区表的执行计划

如果表是分区表(如按时间分区的销售表),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
);

-- 2. 插入测试数据
INSERT INTO sales (sale_id, sale_date, amount)
VALUES
(1, '2022-01-01', 100.50),
(4, '2023-04-10', 300.20),
(8, '2024-09-17', 320.90);

-- 3. 分析查询2024年数据的执行计划
EXPLAIN SELECT * FROM sales WHERE sale_date='2024-09-17';

结果中partitions字段会显示p2024,说明仅扫描2024年的分区,避免了全分区扫描。
在这里插入图片描述

2. 实时分析正在执行的SQL

线上遇到慢查询时,我们可以用EXPLAIN FOR CONNECTION分析正在执行的SQL的执行计划,无需等待SQL执行完成。

步骤:
  1. 模拟慢查询:SELECT *,sleep(100) FROM t1 LIMIT 1;

  2. show processlist查看当前连接的SQL:

    SHOW PROCESSLIST;
    

    会得到类似如下结果,记录目标SQL的Id(如108):
    在这里插入图片描述

  3. 分析该连接的执行计划:

    EXPLAIN FOR CONNECTION 108;  -- 12是上一步的Id
    

这样就能实时定位正在执行的慢查询的性能瓶颈(如是否走索引、是否全表扫描)。
在这里插入图片描述

六、MySQL 8.0新特性:更强大的执行计划

MySQL 8.0对Explain做了增强,新增了树状执行计划EXPLAIN ANALYZE,让分析更精准。

1. 树状执行计划(FORMAT=TREE)

FORMAT=TREE可以以树状结构展示执行计划,更直观地看到查询的逻辑流程,还能显示预估成本

EXPLAIN FORMAT=TREE SELECT * FROM t1 WHERE a=100;

结果示例:
在这里插入图片描述

  • cost=0.35:预估执行成本(数值越小越好);
  • rows=1:预估扫描行数。

2. EXPLAIN ANALYZE:实际执行并返回真实性能数据

EXPLAIN ANALYZE实际执行SQL(注意:避免在生产环境执行写操作!),并返回真实的执行时间、扫描行数、循环次数等数据,比预估更精准:

EXPLAIN ANALYZE SELECT * FROM t1 WHERE a=100;

结果示例:
在这里插入图片描述

  • actual time=0.069..0.071:实际执行时间(毫秒),前值是获取第一行的时间,后值是获取所有行的时间;
  • loops=1:循环次数(避免多次循环导致的性能损耗)。

适用场景:测试环境中精准优化SQL,确认优化后的实际性能提升。

七、总结:Explain优化SQL的核心流程

掌握了Explain后,优化SQL的流程就很清晰了:

  1. 执行EXPLAIN:获取执行计划;
  2. 看type:是否为ALL(全表)或index(全索引),若是则优先加索引;
  3. 看key:是否为NULL(没走索引),若是则检查索引失效原因;
  4. 看Extra:是否有Using filesort/Using temporary,若是则优化排序/GROUP BY字段的索引;
  5. 验证优化:加索引或修改SQL后,再用EXPLAIN(或8.0的EXPLAIN ANALYZE)验证性能是否提升。

最后记住:Explain不是“一次性工具”,而是日常开发中需要频繁使用的“SQL体检仪”——多分析、多对比,才能写出高性能的MySQL查询。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值