MySQL Explain 详解:从入门到精通,让你的 SQL 飞起来

引言:为什么 Explain 是 SQL 优化的 “照妖镜”?

在 Java 开发中,我们常常会遇到数据库性能瓶颈的问题。一条看似简单的 SQL 语句,在数据量增长到一定规模后,可能会从毫秒级响应变成秒级甚至分钟级响应,直接拖慢整个应用的性能。此时,你是否曾困惑于:为什么这条 SQL 突然变慢了?索引明明建了,为什么没生效?到底是哪里出了问题?

答案就藏在 MySQL 的EXPLAIN命令里。EXPLAIN就像一面 “照妖镜”,能帮你看透 SQL 语句的执行计划,揭示 MySQL 是如何处理你的 SQL 的 —— 它会告诉你表的访问顺序、数据读取操作的类型、索引的使用情况、扫描的行数等等关键信息。掌握EXPLAIN的使用方法,是每个 Java 资深技术专家必备的技能,也是优化 SQL 性能的核心武器。

本文将带你全面解锁EXPLAIN的奥秘,从基础概念到实战技巧,从字段解析到案例分析,让你彻底搞懂EXPLAIN,轻松写出高性能的 SQL 语句。

一、MySQL Explain 基础:你必须知道的核心概念

1.1 什么是 Explain?

EXPLAIN是 MySQL 提供的一个诊断工具,它可以模拟 MySQL 优化器执行 SQL 语句的过程,输出 SQL 语句的执行计划。通过分析执行计划,我们可以了解 SQL 的执行细节,从而发现潜在的性能问题,进行针对性优化。

1.2 Explain 的作用

  • 查看表的访问顺序:MySQL 是如何 join 多个表的?先访问哪个表,后访问哪个表?
  • 查看数据的访问方式:是全表扫描还是使用了索引?使用了哪个索引?
  • 查看索引的使用情况:哪些索引被考虑了(possible_keys),哪些索引实际被使用了(key)?
  • 查看扫描的行数(rows):预估需要扫描多少行数据才能得到结果,行数越少性能越好。
  • 查看额外的信息(Extra):如是否使用了临时表、是否进行了文件排序等,这些信息往往是性能问题的关键。

1.3 Explain 的使用方法

使用EXPLAIN非常简单,只需在 SQL 语句前加上EXPLAIN关键字即可。例如:

 
  

EXPLAIN SELECT * FROM user WHERE id = 1;

对于UPDATE、DELETE、INSERT语句,也可以使用EXPLAIN,但需要注意的是,EXPLAIN不会实际执行这些语句,只会分析其执行计划。例如:

 
  

EXPLAIN UPDATE user SET name = '张三' WHERE id = 1;

此外,EXPLAIN FORMAT=JSON可以输出 JSON 格式的执行计划,包含更详细的信息:

 
  

EXPLAIN FORMAT=JSON SELECT * FROM user WHERE id = 1;

二、Explain 输出字段详解:逐个击破每个参数

EXPLAIN的输出通常包含 12 个字段:id、select_type、table、type、possible_keys、key、key_len、ref、rows、filtered、Extra。下面我们逐个解析每个字段的含义和作用。

2.1 id:查询的序列号

id字段表示查询中每个 select 子句的序列号,或者说是查询的执行顺序标识。它有以下几种情况:

  • id 相同:表示这些查询是同一层级的,执行顺序由上至下。
    • 实例
 
  

EXPLAIN SELECT u.name, o.order_no FROM user u JOIN order o ON u.id = o.user_id;

输出中u和o的id相同,说明 MySQL 可能先访问user表,再访问order表(具体顺序可能受优化器影响)。

  • id 不同:id值越大,优先级越高,越先执行。
    • 实例
 
  

EXPLAIN SELECT * FROM user WHERE id = (SELECT user_id FROM order WHERE order_no = '2023001');

子查询的id会大于主查询的id,说明 MySQL 会先执行子查询(SELECT user_id FROM order ...),再执行主查询。

  • id 为 NULL:表示这是一个临时表的操作,不属于任何具体的 select 子句。
    • 实例
 
  

EXPLAIN SELECT * FROM (SELECT id FROM user WHERE age > 18) t;

外层查询的id为 1,内层子查询的结果会被放入临时表t,临时表的id为 NULL。

2.2 select_type:查询类型

select_type字段表示查询的类型,用于区分普通查询、联合查询、子查询等复杂查询。常见的取值有:

  • SIMPLE:简单查询,不包含子查询或UNION。
    • 实例:EXPLAIN SELECT * FROM user; 输出的select_type为 SIMPLE。
  • PRIMARY:主查询,包含子查询时,最外层的查询被标记为 PRIMARY。
    • 实例:EXPLAIN SELECT * FROM user WHERE id = (SELECT user_id FROM order); 主查询的select_type为 PRIMARY。
  • SUBQUERY:子查询中的第一个SELECT(不在FROM子句中)。
    • 实例:上述子查询SELECT user_id FROM order的select_type为 SUBQUERY。
  • DERIVED:派生表,指在FROM子句中包含的子查询,MySQL 会将其结果存放在临时表中。
    • 实例:EXPLAIN SELECT * FROM (SELECT id FROM user) t; 子查询SELECT id FROM user的select_type为 DERIVED。
  • UNION:UNION中的第二个及以后的SELECT语句。
    • 实例:EXPLAIN SELECT id FROM user WHERE age = 18 UNION SELECT id FROM user WHERE age = 20; 第二个SELECT的select_type为 UNION。
  • UNION RESULT:UNION的结果集。
    • 实例:上述UNION查询中,最终合并结果的行的select_type为 UNION RESULT,id为 NULL。

2.3 table:查询的表

table字段表示当前行正在访问的表的名称。如果查询中使用了别名,这里会显示别名。如果是派生表或临时表,可能会显示类似derived2(数字表示对应的子查询 id)的名称。

  • 实例:EXPLAIN SELECT u.name FROM user u; 输出的table字段为u。

2.4 type:访问类型(重中之重)

type字段表示 MySQL 访问表中数据的方式,又称 “访问类型”。它是判断查询性能的重要指标,从好到坏的顺序如下:

system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

下面详细介绍几种常见的类型:

  • system:表中只有一行数据(系统表),是 const 类型的特例,性能最佳。
    • 实例:MySQL 的系统表mysql.proc通常只有少量行,查询时可能出现type=system。
  • const:通过主键或唯一索引查询,最多返回一行数据。MySQL 能将该查询转化为一个常量。
    • 实例:EXPLAIN SELECT * FROM user WHERE id = 1;(id 是主键),type为 const。
  • eq_ref:在JOIN查询中,被驱动表通过主键或唯一非空索引与驱动表关联,且每个索引键只对应一行数据。
    • 实例:EXPLAIN SELECT o.order_no FROM user u JOIN order o ON u.id = o.user_id; 若o.user_id是主键,则o表的type为 eq_ref。
  • ref:使用非唯一索引或唯一索引的前缀进行查询,可能返回多行数据。
    • 实例:EXPLAIN SELECT * FROM user WHERE name = '张三';(name 是非唯一索引),type为 ref。
  • range:使用索引查询指定范围的数据,如BETWEEN、IN、>、<等。
    • 实例:EXPLAIN SELECT * FROM user WHERE id BETWEEN 1 AND 10;(id 是主键),type为 range。
  • index:全索引扫描,遍历整个索引树,比 ALL 快(因为索引文件通常比数据文件小)。
    • 实例:EXPLAIN SELECT id FROM user;(id 是主键索引),MySQL 只需扫描索引树即可,type为 index。
  • ALL:全表扫描,遍历整个表来查找匹配的行,性能最差,应尽量避免。
    • 实例:EXPLAIN SELECT * FROM user WHERE age = 18;(age 无索引),type为 ALL。

优化建议:在实际开发中,应保证查询的type至少达到range级别,最好能达到refconst级别。若出现ALL,通常需要添加或优化索引。

2.5 possible_keys:可能使用的索引

possible_keys字段表示 MySQL 在查询时可能会使用的索引列表。这是 MySQL 根据查询条件估算的可能用到的索引,实际不一定会使用。

  • 实例:表 user 有索引idx_name(name 字段)和idx_age(age 字段),执行EXPLAIN SELECT * FROM user WHERE name = '张三' AND age = 18;,possible_keys可能为idx_name, idx_age。

2.6 key:实际使用的索引

key字段表示 MySQL 在查询时实际使用的索引。如果key为 NULL,说明没有使用索引。

key字段是判断索引是否生效的核心依据。若possible_keys不为空但key为空,可能是因为 MySQL 优化器认为全表扫描比使用索引更快(如表中数据量极少时),或索引失效(如使用了函数操作索引字段)。

  • 实例:接上述例子,若 MySQL 实际使用了idx_name,则key为idx_name。

2.7 key_len:索引使用的长度

key_len字段表示 MySQL 在查询时使用的索引的长度(以字节为单位)。它可以帮助我们判断索引的使用情况,尤其是组合索引的使用情况。

key_len的计算规则与字段类型、字符集、是否为 NULL 有关:

  • 对于CHAR(n)和VARCHAR(n),key_len = n × 字符集字节数 + (是否为 NULL:1 字节,不为 NULL 则 0)。
  • 对于INT,key_len = 4 字节(不为 NULL)。
  • 对于BIGINT,key_len = 8 字节(不为 NULL)。
  • 实例:表 user 的组合索引idx_name_age(name VARCHAR (10) NOT NULL,utf8mb4 字符集;age INT NOT NULL)。
    • 若查询条件为name = '张三',则key_len = 10×4 = 40 字节(仅使用了组合索引的 name 部分)。
    • 若查询条件为name = '张三' AND age = 18,则key_len = 10×4 + 4 = 44 字节(使用了组合索引的全部部分)。

通过key_len可以判断组合索引是否被充分利用:key_len越大,说明使用的索引部分越多。

2.8 ref:与索引比较的列或常量

ref字段表示在使用索引查询时,哪些列或常量被用来与索引进行比较。

  • 实例
    • EXPLAIN SELECT * FROM user WHERE name = '张三'; 中,ref为const(表示与常量比较)。
    • EXPLAIN SELECT o.* FROM user u JOIN order o ON u.id = o.user_id; 中,o表的ref为test.u.id(表示与 user 表的 id 列比较)。

2.9 rows:预估扫描行数

rows字段表示 MySQL 预估需要扫描的行数,用于评估查询的代价。rows值越小,查询效率越高。

注意:rows是一个预估值,不是实际扫描的行数,但通常能反映查询的大致效率。优化时,应尽量减少rows的值。

  • 实例:EXPLAIN SELECT * FROM user WHERE id = 1;(id 是主键),rows通常为 1;而全表扫描时,rows接近表的总记录数。

2.10 filtered:过滤比例

filtered字段表示符合查询条件的记录占扫描行数的百分比(范围 0-100)。filtered值越大,说明过滤效果越好,无用数据扫描越少。

rows × filtered / 100 可以估算出最终返回的记录数。

  • 实例:rows为 1000,filtered为 10,则预估返回 100 行记录。

2.11 Extra:额外信息(重要补充)

Extra字段包含了 MySQL 执行查询时的额外信息,这些信息对于分析查询性能非常重要。常见的取值有:

  • Using index:使用了覆盖索引(查询的字段都在索引中,无需回表查询数据),性能极佳。
    • 实例:EXPLAIN SELECT id, name FROM user WHERE name = '张三';(idx_name包含 id 和 name),Extra为 Using index。
  • Using where:MySQL 使用了 WHERE 条件过滤数据,但未使用索引(或索引未覆盖所有查询条件)。
    • 实例:EXPLAIN SELECT * FROM user WHERE age = 18;(age 无索引),Extra为 Using where。
  • Using filesort:MySQL 需要额外的排序操作,且排序未使用索引,而是在内存或磁盘中完成(文件排序)。这通常是性能瓶颈,应尽量避免。
    • 实例:EXPLAIN SELECT * FROM user ORDER BY name;(name 无索引),Extra为 Using filesort。优化方法:为 name 添加索引。
  • Using temporary:MySQL 需要创建临时表来存储中间结果(如 GROUP BY、DISTINCT 等操作)。临时表会消耗额外的 CPU 和内存,应尽量避免。
    • 实例:EXPLAIN SELECT name FROM user GROUP BY name;(name 无索引),Extra为 Using temporary。优化方法:为 name 添加索引。
  • Using join buffer:JOIN查询中,未使用索引,MySQL 使用了连接缓冲区来存储中间结果。性能较差,应添加关联字段的索引。
    • 实例:EXPLAIN SELECT * FROM user u JOIN order o ON u.name = o.user_name;(u.name 和 o.user_name 无索引),Extra为 Using join buffer。
  • Impossible WHERE:WHERE 条件永远为假,不会返回任何结果。
    • 实例:EXPLAIN SELECT * FROM user WHERE 1 = 0;,Extra为 Impossible WHERE。
  • Range checked for each record (index map: N):MySQL 没有找到合适的索引,会为每一行记录检查是否符合条件,性能极差,通常需要添加合适的索引。

三、Explain 实战:通过案例掌握 SQL 优化技巧

理论结合实践才能真正掌握EXPLAIN。下面通过多个实战案例,展示如何使用EXPLAIN分析并优化 SQL 语句。

3.1 案例 1:全表扫描优化(type=ALL)

场景:查询年龄为 18 的用户信息,表 user 有 100 万行数据,age 字段无索引。

SQL 语句

 
  

SELECT * FROM user WHERE age = 18;

Explain 输出

id

select_type

table

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

user

ALL

NULL

NULL

NULL

NULL

1000000

10.00

Using where

分析:type=ALL(全表扫描),key=NULL(未使用索引),rows=1000000(扫描 100 万行),性能极差。

优化方案:为 age 字段添加索引。

 
  

ALTER TABLE user ADD INDEX idx_age (age);

优化后 Explain 输出

id

select_type

table

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

user

ref

idx_age

idx_age

4

const

10000

100.00

效果:type=ref(使用索引),rows=10000(扫描行数大幅减少),性能显著提升。

3.2 案例 2:避免 Using filesort(文件排序)

场景:查询用户信息并按 name 排序,name 字段无索引。

SQL 语句

 
  

SELECT * FROM user WHERE age = 18 ORDER BY name;

Explain 输出

id

select_type

table

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

user

ref

idx_age

idx_age

4

const

10000

100.00

Using where; Using filesort

分析:Extra为Using filesort,表示需要额外的排序操作,性能较差。

优化方案:创建组合索引idx_age_name(age, name),既满足查询条件,又支持排序。

 
  

ALTER TABLE user DROP INDEX idx_age;

ALTER TABLE user ADD INDEX idx_age_name (age, name);

优化后 Explain 输出

id

select_type

table

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

user

ref

idx_age_name

idx_age_name

4

const

10000

100.00

Using index condition

效果:Extra中Using filesort消失,排序操作通过索引完成,性能提升。

3.3 案例 3:消除 Using temporary(临时表)

场景:查询用户姓名并去重,name 字段无索引。

SQL 语句

 
  

SELECT DISTINCT name FROM user;

Explain 输出

id

select_type

table

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

user

ALL

NULL

NULL

NULL

NULL

1000000

100.00

Using temporary

分析:Extra为Using temporary,表示需要创建临时表存储去重结果,性能较差。

优化方案:为 name 字段添加索引,索引天然有序且不重复,可避免临时表。

 
  

ALTER TABLE user ADD INDEX idx_name (name);

优化后 Explain 输出

id

select_type

table

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

user

index

NULL

idx_name

42

NULL

1000000

100.00

Using index

效果:Extra为Using index(覆盖索引),Using temporary消失,性能提升。

3.4 案例 4:子查询优化

场景:查询有订单的用户信息,使用子查询。

SQL 语句

 
  

SELECT * FROM user WHERE id IN (SELECT user_id FROM order);

Explain 输出(可能的结果):

id

select_type

table

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

PRIMARY

user

ALL

PRIMARY

NULL

NULL

NULL

100000

100.00

Using where

2

SUBQUERY

order

index

idx_user_id

idx_user_id

4

NULL

500000

100.00

Using index

分析:主查询type=ALL(全表扫描),性能较差。MySQL 优化器对某些子查询的优化不够理想,可能导致全表扫描。

优化方案:将子查询改为JOIN查询,通常性能更好。

 
  

SELECT DISTINCT u.* FROM user u JOIN order o ON u.id = o.user_id;

优化后 Explain 输出

id

select_type

table

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

u

index

PRIMARY

PRIMARY

4

NULL

100000

100.00

Using index

1

SIMPLE

o

ref

idx_user_id

idx_user_id

4

test.u.id

5

100.00

Using index

效果:主查询type=index,通过JOIN利用索引,扫描行数减少,性能提升。

四、Explain 进阶:特殊场景与高级技巧

4.1 分析 UPDATE 和 DELETE 语句

EXPLAIN不仅适用于 SELECT 语句,也适用于 UPDATE 和 DELETE 语句。分析这些语句的执行计划,可以避免因更新 / 删除操作导致的全表扫描,影响数据库性能。

  • 实例:分析删除操作
 
  

EXPLAIN DELETE FROM user WHERE age = 18;

若输出type=ALL,说明会全表扫描删除,应添加 age 索引;若type=ref,则使用了索引,性能更优。

4.2 分析联合索引的最左前缀原则

联合索引遵循最左前缀原则:即索引的生效顺序从左到右,若查询条件不包含最左列,则索引不生效。通过EXPLAIN可以验证这一点。

  • 实例:联合索引idx_age_name(age, name)
    • EXPLAIN SELECT * FROM user WHERE age = 18 AND name = '张三';:key=idx_age_name(全索引生效)。
    • EXPLAIN SELECT * FROM user WHERE name = '张三';:key=NULL(索引不生效,因缺少最左列 age)。

4.3 识别索引失效的场景

通过EXPLAIN的key字段,可以识别索引失效的场景,常见的有:

  • 索引字段使用函数:WHERE SUBSTR(name, 1, 1) = '张'(name 有索引,但key=NULL)。
  • 索引字段参与运算:WHERE id + 1 = 10(id 有索引,但key=NULL)。
  • 使用不等于(!=、<>)、NOT IN、IS NOT NULL:可能导致索引失效(视数据分布而定)。
  • 字符串不加引号:WHERE name = 123(name 是字符串类型,可能导致索引失效)。

优化方法:避免在索引字段上使用函数或运算,尽量使用等于(=)、IN等操作。

4.4 使用 Explain FORMAT=JSON 获取更详细信息

EXPLAIN FORMAT=JSON输出的 JSON 格式执行计划包含更详细的信息,如成本估算(cost_info)、数据读取方式(access_type)等,适合深入分析复杂查询。

  • 实例
 
  

EXPLAIN FORMAT=JSON SELECT * FROM user WHERE id = 1;

输出结果中包含"cost_info": {"query_cost": "1.00"}(查询成本)、"access_type": "const"等信息。

五、总结:让 Explain 成为你的 SQL 优化利器

MySQL 的EXPLAIN命令是 SQL 优化的 “瑞士军刀”,它能帮你看透 SQL 的执行本质,发现潜在的性能问题。通过本文的学习,你应该已经掌握了EXPLAIN的各个字段的含义,以及如何利用它来分析和优化 SQL 语句。

核心要点回顾

  1. type和Extra是判断查询性能的关键指标,应尽量避免ALL、Using filesort、Using temporary。
  1. 索引是优化 SQL 的核心,通过EXPLAIN的key字段可以验证索引是否生效。
  1. 联合索引遵循最左前缀原则,key_len可帮助判断索引的使用情况。
  1. 对于复杂查询(子查询、JOIN、UNION),id和select_type字段有助于分析执行顺序。

在实际开发中,养成写 SQL 前先使用EXPLAIN分析的习惯,能帮你提前规避性能问题。记住,好的 SQL 不是写出来的,而是优化出来的,而EXPLAIN就是你优化之路上的最佳伙伴。

希望本文能让你对EXPLAIN有更深入的理解,让你的 SQL 性能更上一层楼!如果你有更多关于EXPLAIN的使用技巧或实战案例,欢迎在评论区分享交流。

你可能感兴趣的:(MySQL Explain 详解:从入门到精通,让你的 SQL 飞起来)