EXPLAIN 执行计划
有了慢查询语句后,就要对语句进行分析。一条查询语句在经过 MySQL 查询优化器的各种基于成本和规则的优化会后生成一个所谓的执行计划,这个执行计划展示了接下来具体执行查询的方式,比如多表连接的顺序是什么,对于每个表采用什么访问方法来具体执行查询等等。EXPLAIN 语句来能够查看某个查询语句的具体执行计划,要搞懂 EPLATN 的各个输出项都有什么作用,从而可以有针对性的提升查询语句的性能。
通过使用 EXPLAIN 关键字可以模拟优化器执行 SQL 查询语句,从而知道 MySQL 是如何处理 SQL 语句的。分析查询语句或是表结构的性能瓶颈。
EXPLAIN 可以得到以下结果:
- 表的读取顺序;
- 数据读取操作的操作类型;
- 哪些索引可以使用;
- 哪些索引被实际使用;
- 表之间的引用;
- 每张表有多少行被优化器查询;
EXPLAIN 基本语法
执行计划的语法非常简单:在 SQL 查询的前面加上 EXPLAIN 关键字。比如:EXPLAIN select * from tableName;
除了以 SELECT 开头的查询语句,其余的 DELETE、INSERT、UPOATE 语句前边都可以加上 EXPLAIN,用来查看这些语句的执行计划,只不过大多数情况下都会对 SELECT 语句更感兴趣。
EXPLAIN 详解
执行 sql explain SELECT * FROM order_test;
输出结果如图:
![](http://e1idc.net/wp-content/uploads/2023/12/frc-d0eb782bffdbfd94338508d784bbd334.png)
每列的含义
id
查询语句一般都以 SELECT 关键字开头,比较简单的查询语句里只有一个 SELECT 关键字,稍微复杂一点的连接查询中也只有一个 SELECT 关键字,比如:
explain SELECT * FROM test1 t1 INNER JOIN test2 t2 ON t1.id=t2.id ;
但是下边两种情况下在一条查询语句中会出现多个 SELECT 关键字:
查询中包含子查询的情况:explain SELECT * FROM test1 WHERE id IN(SELECT id FROM test2);
查询中包含 UNION / UNION ALL 语句的情况:EXPLAIN SELECT * FROM test1 UNION ALL SELECT * FROM test2;
查询语句中每出现一个 SELECT 关键字,MySQL 就会为它分配一个唯一的 id 值。这个 id 值就是 EXPLAIN 语句的第一列的值,并且 id 的顺序是按 SELECT 出现的顺序增长的,id 列越大执行优先级越高,id 相同则从上往下执行,id 为 NULL 最后执行。
单 SELECT 关键字
比如下边这个查询中只有一个 SELECT 关键字,所以 EXPLAIN 的结果中也就只有一条 id 列为 1 的记录。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-936df2d08230d141651931862d5a9d57.png)
连接查询 对于连接查询来说,一个 SELECT 关键字后边的 FROM 子句中可以跟随多个表,所以在连接查询的执行计划中,每个表都会对应一条记录,但是这些记录的 id 值都是相同的。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-8bec47b0634ae2ec2e86f471c920f0bd.png)
可以看到,上述连接查询中参与连接的 test1 和 test2 表分别对应一条记录,但是这两条记录对应的 id 值都是 1,在连接查询的执行计划中,每个表都会对应一条记录,这些记录的 id 列的值是相同的。
包含子查询
对于包含子查询的查询语句来说,就可能涉及多个 SELECT 关键字,所以在包含子查询的查询语句的执行计划中,每个 SELECT 关键字都会对应一个唯一的 id 值。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-abdbcd401e192ca584e1abcb051d1532.png)
但是这里需要特别注意,查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询。所以如果我们想知道查询优化器对某个包含子查询的语句是否进行了重写,直接查看执行计划。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-93e7904a6014b4a9fb22ba0a20c59911.png)
可以看到,虽然查询语句是一个子查询,但是执行计划中 test1 和 test2 表对应的记录的 id 值全部是 1,这就表明了查询优化器将子查询转换为了连接查询。
包含 UNION UNION ALL 子句
对于包含 UNION 子句的查询语句来说,每个 SELECT 关键字对应一个 id 值也是没错的,不过还是有点儿特别。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-9c4f16e7bbe4033cc0db4643b0793070.png)
这个语句的执行计划的第三条记录是因为 UNION 子句会把多个查询的结果集合并起来并对结果集中的记录进行去重,MySQL 使用的是内部的临时表。UNION 子句是为了把 id 为 1 的查询和 id 为 2 的查询的结果集合并起来并去重,所以在内部创建了一个名为 的临时表,就是执行计划第三条记录的 table 列的名称,id 为 NULL 表明这个临时表是为了合并两个查询的结果集而创建的。
跟 UNION 对比起来 UNION ALL 就不需要为最终的结果集进行去重,它只是单纯的把多个查询的结果集中的记录合并成一个并返回给用户,所以也就不需要使用临时表。所以在包含 UNION ALL 子句的查询的执行计划中,就没有那个 id 为 NULL 的记录。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-e6043786927115115455c2b559b9b684.png)
select_type
一条大的查询语句里边可以包含若干个 SELECT 关键字,每个 SELECT 关键字代表着一个小的查询语句,而每个 SELECT 关键字的 FROM 子句中都可以包含若干张表,每一张表都对应着执行计划输出中的一条记录,对于在同一个 SELECT 关键字中的表来说,它们的 id 值是相同的。
MySQL 为每一个 SELECT 关键字代表的小查询都定义了一个称之为:select_type 的属性,意思是我们只要知道了某个小查询的 select_type 属性,就知道了这个小查询在整个大查询中扮演了一个什么角色,select_type 取值如下:
- SIMPLE:简单的 SELECT 查询,不使用 union 及子查询;
- PRIMARY:最外层的 SELECT 查询;
- UNION:UNION 中的第二个或随后的 SELECT 查询,不依赖于外部查询的结果集;
- UNION RESULT:UNION 结果集;
- SUBQUERY:子查询中的第一个 SELECT 查询,不依赖于外部查询的结果集;
- DERIVED: 用于 FROM 子句里有子查询的情况,MySQL 会递归执行这些子查询,把结果放在临时表里;
SIMPLE
简单的 select 查询,查询中不包含子查询或者 UNION。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-55a5496c7ef7f02c48e857fabecb2aa0.png)
连接查询也算是 SIMPLE 类型。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-7eefd52142a315675b8cd4fb1010d13f.png)
PRIMARY
对于包含 UNION、UNION ALL 或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的 select_type 值就是 PRIMARY。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-3ddab74fed55e2286af09ae89edfb39e.png)
从结果中可以看到,最左边的小查询 SELECT * FROMN test1
对应的是执行计划中的第一条记录,它的 select_type 值就是 PRIMARY。
UNION
对于包含 UNION 或者 UNION ALL 的大查询来说,它是由几个小查询组成的,其中除了最左边的那个小查询以外,其余的查询的 select_type 值就是 UNION。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-98e00bc5a56d9aee99104d33e8c21800.png)
UNION RESULT
MySQL 选择使用临时表来完成 UNION 查询的去重工作,针对该临时表的查询的 select_type 就是 UNION RESULT,如上图。
SUBQUERY
包含在 SELECT 中的子查询,不在 FROM 子句中。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-92a1d04e37a77563035241e4b0e5d2ab.png)
DERIVED
包含在 FROM 子句中的子查询。MySQL 会将结果存放在一个临时表中,也称为派生表。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-c778364acbcf7cdab88100d7aaa77793.png)
从执行计划中可以看出, id 为 2 的记录就代表子查询的执行方式,它的 select_type 是 DERIVED ,说明该子查询是以派生表的方式执行的。id 为 1 的记录代表外层查询,注意看它的 table 列显示的是 ,表示该查询是针对将派生表之后的表进行查询的。
table
不论我们的查询语句有多复杂,里边包含了多少个表,到最后也是需要对每个表进行单表访问的,MySQL 规定 EXPLAIN 语句输出的每条记录都对应着某个单表的访问方法,该条记录的 table 列代表着该表的表名。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-9e59d1ee4d7043b9f681920ffc8ed254.png)
只涉及对 test1 表的单表查询,所以 EXPLAIN 输出中只有一条记录,其中的 table 列的值是 test1;
![](http://e1idc.net/wp-content/uploads/2023/12/frc-600d11ddf67e670b08aaecc59bc25dd2.png)
连接查询的执行计划中有两条记录,这两条记录的 table 列分别是 test1和 test2。
partitions
和分区表有关,一般情况下查询语句的执行计划的 partitions 列的值都是NULL。
type
执行计划的一条记录就代表着 MySQL 对某个表的执行查询时的访问方法或访问类型,其中的 type 列就表明了这个访问方法或访问类型是较为重要的一个指标,结果值从最好到最坏依次是:system > const > eq_ref > ref > range > index > ALL
。
一般来说,得保证查询至少达到 range 级别,最好能达到 ref。
system
当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如:MyISAM、Memory,那么对该表的访问方法就是 system。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-984beb5691277f0b06d1daa477a4b693.png)
如果改成使用 InnoDB 存储引擎,type 的值就是 all。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-050893f06dce6149d770e4e8a9950596.png)
const
根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是 const。因为只匹配一行数据,所以很快。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-cc8b51a2b4c7f5adc0d21046a48228e4.png)
B+ Tree 叶子节点中的记录是按照索引列排序的,对于的聚簇索引来说,它对应的B+树叶子节点中的记录就是按照id列排序的。B+树矮胖,所以这样根据主键值定位一条记录的速度很快。类似的,我们根据唯一二级索引列来定位一条记录的速度也很快的,比如下边这个查询:
eq_ref
在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的,如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较,则对该被驱动表的访问方法就是:eq_ref。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-c37d44433aa24f0ff3cb8e1465ba3b98.png)
从执行计划的结果中可以看出,MySQL 打算将 test2 作为驱动表,test1 作为被驱动表,重点关注 test1 的访问方法是 eq_ref,表明在访问 test1 表的时候可以通过主键的等值匹配来进行访问。
驱动表与被驱动表:A 表和 B 表 join 连接查询,如果通过 A 表的结果集作为循环基础数据,然后一条一条地通过该结果集中的数据作为过滤条件到 B 表中查询数据,然后合并结果。那么我们称 A 表为驱动表,B 表为被驱动表。
ref
当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是 ref。
本质上也是一种索引访问,它返回所有匹配某个单独值的行,它可能会找到多个符合条件的行,所以他属于查找和扫描的混合体。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-e970ac4cc2a5bf1414a03bf4f5ec2588.png)
对于这个查询可以选择全表扫描来逐一对比搜索条件是否满足要求,也可以先使用二级索引找到对应记录的 id 值,然后再回表到聚簇索引中查找完整的用户记录。
由于普通二级索引并不限制索引列值的唯一性,所以可能找到多条对应的记录,也就是说使用二级索引来执行查询的代价取决于等值匹配到的二级索引记录条数。
如果匹配的记录较少,则回表的代价还是比较低的,所以 MySQL 可能选择使用索引而不是全表扫描的方式来执行查询。这种搜索条件为二级索引列与常数等值比较,采用二级索引来执行查询的访问方法称为:ref。
对于普通的二级索引来说,通过索引列进行等值比较后可能匹配到多条连续的记录,而不是像主键或者唯一二级索引那样最多只能匹配 1 条记录,所以这种 ref 访问方法比 const 要差些,但是在二级索引等值比较时匹配的记录数较少时的效率还是很高的,如果匹配的二级索引记录太多那么回表的成本就太大了。
对于某个包含多个索引列的二级索引来说,只要是最左边的连续索引列是与常数的等值比较就可能采用 ref 的访问方法。
range
如果使用索引获取某些范围区间的记录,那么就可能使用到 range 访问方法,一般就是在 where 语句中出现了 between
、、
>
、in
等的查询。
这种范围扫描索引比全表扫描要好,因为它只需要开始于索引的某一点,而结束语另一点,不用扫描全部索引。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-9ec00891c72f66403622e7e8a6356bd4.png)
这种利用索引(聚簇索引、二级索引)进行范围匹配的访问方法称之为:range。
index
可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是:index。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-ddc38139e6cb22bb1eb54e37093b12c2.png)
ALL
最熟悉的全表扫描,将遍历全表以找到匹配的行。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-cbd390b65f939a4cc7b195b8f8ac61d8.png)
possible_keys 和 key
在 EXPLAIN 语句输出的执行计划中,possible_keys 列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些。key 列表示实际用到的索引有哪些,如果为 NULL,则没有使用索引。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-8bfb5019c93c2c172ff6f6b4a23450fe.png)
上述执行计划的 possible_keys 列的值表示该查询可能使用到 u_idx_day_status、idx_insert_time 两个索引,然后 key 列的值是 u_idx_day_status,表示经过查询优化器计算使用不同索引的成本后,最后决定使用 u_idx_day_status 来执行查询比较划算。
key_len
key_len 列,表示当优化器决定使用某个索引执行查询时,该索引记录的最大长度,计算方式如下:
- 对于使用固定长度类型的索引列来说,它实际占用的存储空间的最大长度就是该固定值,对于指定字符集的变长类型的索引列来说,如:某个索引列的类型是VARCHAR(100),使用的字符集是 utf8,那么该列实际占用的最大存储空间就是100×3=300个字节;
- 如果该索引列可以存储 NULL 值,则 key_len 比不可以存储 NULL 值时多 1 个字节;
- 对于变长字段来说,都会有 2 个字节的空间来存储该变长列的实际长度;
![](http://e1idc.net/wp-content/uploads/2023/12/frc-3b96ede637baeef6a53b4ef0cb60f9af.png)
由于 id 列的类型是 bigint,并且不可以存储 NULL 值,所以在使用该列的索引时 key_len 大小就是 8。
对于可变长度的索引列来说。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-2223454c8048d67a1c9ee09009ee3dc0.png)
由于 order_no 列的类型是 VARCHAR(50),所以该列实际最多占用的存储空间就是 50*3 字节,又因为该列是可变长度列,所以 key_len 需要加2,所以最后ken_len 的值就是 152。
执行计划的生成是在 MySQL server 层中的功能,并不是针对具体某个存储引擎的功能,MySQL 在执行计划中输出 key_len 列主要是为了区分某个使用联合索引的查询具体用了几个索引列,而不是为了准确的说明针对某个具体存储引擎存储变长字段的实际长度占用的空间到底是占用几个字节。
key_len 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好。
key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的。
注:char 和 varchar 跟字符编码也有密切的联系,比如 latin1 占用 1 个字节,gbk 占用 2 个字节,utf8 占用 3 个字节。
ref
当使用索引列等值查询时,与索引列进行等值匹配的对象信息;
![](http://e1idc.net/wp-content/uploads/2023/12/frc-8a8bc7934b92d672743b29dcfbacc6c4.png)
可以看到 ref 列的值是 const,表明在使用 idx_order_no 索引执行查询时,与 order_no 列作等值匹配的对象是一个常数。
复杂的情况。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-99d547c4c9695d414fa063cd5aef1ab0.png)
可以看到对被驱动表 t1 的访问方法是 eg_ref,而对应的 ref 列的值是 test.t2.id,这说明在对被驱动表进行访问时会用到 PRIMARY 索引,也就是聚簇索引与一个列进行等值匹配的条件,与 t2 表的 id 作等值匹配的对象就是 test.t2.id列,格式:数据库名称.表名.字段。
rows
如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的 rows 列就代表预计需要扫描的行数,如果使用索引来执行查询时,执行计划的 rows 列就代表预计扫描的索引记录行数。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-68d916266196b17c915f70285a87a3cd.png)
filtered
某个表经过搜索条件过滤后剩余记录条数的百分比;
![](http://e1idc.net/wp-content/uploads/2023/12/frc-59861b8b66bb32b06953f085e38352fd.png)
从执行计划的 key 列中可以看出来,该查询使用 PRIMARY 索引来执行查询,从 rows 列可以看出满足 id>5890
的记录有 5177 条。执行计划的 filtered 列就代表查询优化器预测在这 5177 条记录中,有多少条记录满足其余的搜索条件,也就是 order_note=’a’ 这个条件的百分比。此处 filtered 列的值是 10.0,说明查询优化器预测在 5177 条记录中有 10.00% 的记录满足 order_note=’a’ 这个条件。
Extra
Extra 列是用来说明一些额外信息的,可以通过这些额外信息来更准确的理解 MySQL 到底将如何执行给定的查询语句。MySQL 提供的额外信息很多,常见的重要值如下:
Using index:当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用索引覆盖的情况下,在 Extra 列将会提示该额外信息。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-e5c94e754d510fbc9b0f342c5581e3a1.png)
这个查询中只需要用到 u_idx_day_status 而不需要回表操作。
Using where:当我们使用全表扫描来执行对某个表的查询,并且该语句的 WHERE 子句中有针对该表的搜索条件时,在 Extra 列中会提示。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-952e92944ed9d05f43ea125c6d25f681.png)
Using where 只是表示 MySQL 使用 where 子句中的条件对记录进行了过滤。
Using index condition:有些搜索条件中虽然出现了索引列,但却不能使用到索引。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-286ef86b7d3054c0757a12abff06691a.png)
其中的 order_no >'z'
可以使用到索引,但是 order_no LIKE '%a'
却无法使用到索引,在以前版本的 MySQL 中,是按照下边步骤来执行这个查询的:
- 先根据 order_no>’z’ 这个条件,从二级索引 idx_order_no 中获取到对应的二级索引记录;
- 根据上一步骤得到的二级索引记录中的主键值进行回表,找到完整的用户记录再检测该记录是否符合 order_no LIKE ‘%a’ 这个条件,将符合条件的记录加入到最后的结果集;
虽然 order_no LIKE '%a'
不能组成范围区间参与 range 访问方法的执行,但这个条件毕竟只涉及到了 order_no 列,MySQL 把上边的步骤改进了一下。
索引条件下推
- 先根据 order_no>’z’ 这个条件,定位到二级索引 idx_order_no 中对应的二级索引记录;
- 对于指定的二级索引记录,先不着急回表,而是先检测一下该记录是否满足 order_no LIKE ‘%a’ 这个条件,如果这个条件不满足,则该二级索引记录压根儿就没必要回表;
- 对于满足 order_no LIKE ‘%a’ 这个条件的二级索引记录执行回表操作,回表操作其实是一个随机 IO 比较耗时;
所以上述修改可以省去很多回表操作的成本,这个改进称之为索引条件下推。
如果在查询语句的执行过程中将要使用索引条件下推这个特性,在 Extra 列中将会显示 Using index condition。
Using temporary:在许多查询的执行过程中,MySQL 可能会借助临时表来完成一些功能,比如去重、排序之类的,比如在执行许多包含 DISTINCT、GROUPBY、UNION 等子句的查询过程中,如果不能有效利用索引来完成查询,MySQL 很有可能寻求通过建立内部的临时表来执行查询。如果查询中使用到了内部的临时表,在执行计划的 Extra 列将会显示 Using temporary。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-29628a3f0046ef84bdbe779314e073e5.png)
![](http://e1idc.net/wp-content/uploads/2023/12/frc-3b8d6667dc007db920ad9fb6edfc8e49.png)
上边的 GROUP BY 的执行计划的 Extra 列不仅仅包含 Using temporary 提示,还包含 Using filesort 提示,可是查询语句中明明没有写 ORDER BY 子句,这是因为 MySQL 会在包含 GROUP BY 子句的查询中默认添加上 ORDER BY 子句。
如果不想为包含 GROUP BY 子句的查询进行排序,需要显式的写上 ORDER BY NULL。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-f0acd3fba7adcd18afd2984a33fc316d.png)
Using filesort:有一些情况下对结果集中的记录进行排序是可以使用到索引的。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-56fc00c26eec3cb162d292dfc3fcc80b.png)
这个查询语句可以利用 idx_order_no 索引直接取出 order_no 列的 10 条记录,然后再进行回表操作。但是很多情况下排序操作无法使用到索引,只能在内存中或者磁盘中进行排序,MySQL 把这种在内存中或者磁盘上进行排序的方式统称为文件排序。如果某个查询需要使用文件排序的方式执行查询,就会在执行计划的Extra 列中显示 Using filesort。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-3245fef2b5f3160e235fcee7bde90048.png)
Select tables optimized away:使用某些聚合函数,比如:max、min 来访问存在索引的某个字段是。
![](http://e1idc.net/wp-content/uploads/2023/12/frc-2eaf0f7e8b27594f7ab78c1fc554fadb.png)
作者:京东物流 张士欣
来源:京东云开发者社区 自猿其说Tech 转载请注明来源