Skip to content

SQL执行计划——Explain

使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈。

通过EXPLAIN分析某条SQL语句执行时的如下特征:

  • 表的读取顺序(涉及到多张表时)
  • 数据读取操作的操作类型
  • 哪些索引可以使用
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少行被优化器查询

格式为:explain <SQL语句>img.png

字段含义
idselect查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序
select_type查询类型 或者是 其他操作类型
table正在访问哪个表
partitions匹配的分区信息
type访问方式
possible_keys显示可能应用在这张表中的索引,一个或多个,但不一定实际使用到
key实际使用到的索引,如果为NULL,则没有使用索引
key_len表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度
ref显示索引的哪一列被使用了,如果可能的话,是一个常数,哪些列或常量被用于查找索引列上的值
rows根据表统计信息及索引选用情况,大致估算出找到所需的记录所需读取的行数
filtered表示符合查询条件的数据百分比
Extra附加信息

id ⭐(select唯一标识)

select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序。 根据id是否相同可以分为下列三种情况:

  • 所有表项的id相同,如: img.png 则上表中的3个表项按照从上到下的顺序执行,如读表顺序为t1,t3,t2。 由第一节提到的SQL解析顺序也可验证,首先from t1,t2,t3表明此次查询设计到的表, 由于没有join,接着解析where时开始读表,值得注意的是并不是按照where书写的顺序, 而是逆序,即先解析t1.other_column=''于是读表t1,然后t1.id=t3.id读表t3, 最后t1.id=t2.id读表t2。解析顺序如下:
sql
from
	t1,t2,t3
where
	t1.other_column='', t1.id=t3.id, t1.id=t2.id
select
	t2.*
  • 所有表项的id不同,嵌套查询,id的序号会递增,id值越大优先级越高,越先被执行。类似子查询,有执行依赖关系的, id的序号会递增,id值越大优先级越高,越先被执行如: img.png 对于多层嵌套的查询,执行顺序由内而外。解析顺序:
sql
from
	t2
where
	t2.id=
	from
		t1
	where
		t1.id=
		from
			t3
		where
			t3.other_column=''
		select
		 	t3.id
	select
		t1.id
select
	t2.*

由第12,8,4行可知查表顺序为t3,t1,t2

  • 有的表项id相同,有的则不同。id相同的表项遵循结论1,不同的则遵循结论2 img.png 解析顺序:
sql
from
    (
    from
		t3
	where
		t3.other_column=''
	select
		t3.id
     ) s1, t2	#s1是衍生表
where
	s1.id=t2.id
select
	t2.*

select_type(select类型)

该列常出现的值如下:

查询类型作用
SIMPLE简单查询(未使用UNION或子查询)
UNION在UNION中的第二个和随后的SELECT被标记为UNION。如果UNION被FROM子句中的子查询包含,那么它的第一个SELECT会被标记为DERIVED。 union右侧的select
UNION RESULTUNION的结果
PRIMARY查询中若包含任何复杂的子部分,最外层查询被标记为PRIMARY
DEPENDENT UNIONUNION中的第二个或后面的查询,依赖了外面的查询
SUBQUERY子查询中的第一个 SELECT,在selectwhere列表中包含的子查询
DEPENDENT SUBQUERY子查询中的第一个 SELECT,依赖了外面的查询
DERIVED用来表示包含在FROM子句的子查询中的SELECT,MySQL会递归执行并将结果放到一个临时表中。MySQL内部将其称为是Derived table(派生表),因为该临时表是从子查询派生出来的
DEPENDENT DERIVED派生表,依赖了其他的表
MATERIALIZED物化子查询
UNCACHEABLE SUBQUERY子查询,结果无法缓存,必须针对外部查询的每一行重新评估
UNCACHEABLE UNIONUNION属于UNCACHEABLE SUBQUERY的第二个或后面的查询

table(表名称)

表名,表示该表项是关于哪张表的,也可以是如形式:

  • <derived,N>,表示该表是表项idN的衍生表
  • <unionM,N>,表示该表是表项idMN两者union之后的结果

partition(匹配的分区)

如果启用了表分区策略,则该字段显示可能匹配查询的记录所在的分区

type 🌟(连接类型)

type显示的是访问类型,是较为重要的一个指标,结果值从最好到最坏依次是: system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL 。

type值含义例子
NULL不访问任何表或者索引explain select 1
system表只有一行记录,是const类型的特例
const针对主键或唯一索引的等值查询扫描, 最多只返回一行数据explain select * from s1 where id = 10002;
eq_ref用于联表查询,索引必须是主键或者唯一索引explain select * from s1 left join s2 on s1.id = s2.id;
ref使用普通索引进行单表或者联表查找,支持最左前缀原则explain select * from s1 where key1 = 'cxw';
fulltext全文索引
ref_or_null类似于ref,但是相比于ref额外多了去查NULL的数据explain select * from s1 where key1 = 'cxw' or key1 is null;
index_merge索引合并优化,表示一个查询里面用到了多个索引explain select * from s1 where key1 = 'cxw' or key3 = 'cxw2';
unique_subquery和eq_ref类似,但是使用了IN子查询,且子查询用的主键或者唯一索引EXPLAIN SELECT * FROM s1 WHERE key2 IN (SELECT id FROM s2 where s1.key1 = s2.key1) OR key3 = 'a'
index_subquery和unique_subquery类似,唯一区别是子查询是用的是普通二级索引EXPLAIN SELECT * FROM s1 WHERE common_field IN (SELECT key3 FROM s2 where s1.key1 = s2.key1) OR key3 = 'a';
range范围扫描,表示检索了指定范围的行,主要用于有限制的索引扫描。比较常见的范围扫描是带有BETWEEN子句或WHERE子句里有**>、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE、IN()**等操作符。EXPLAIN SELECT * FROM s1 WHERE key1 IN ('a', 'b', 'c');
index遍历索引树,常见的就是“覆盖索引的场景EXPLAIN SELECT key_part2 FROM s1 WHERE key_part3 = 'a';
ALL全表扫描, 性能最糟糕EXPLAIN SELECT * FROM s1;

按照表格顺序,从上向下,性能从好到坏排序 根据阿里巴巴开发手册要求,SQL性能优化的目标:至少要达到 range 级别,要求是 ref 级别,最好是 const级别。

  • system表只有一行记录(等于系统表),这是const类型的特列,平时不会出现,这个也可以忽略不计
  • const,表示通过索引一次就找到了,const用于比较primary key或者unique key
  • 因为只匹配一行数据,所以很快。若将主键置于where列表中,MySQL就能将该查询转换为一个常量
sql
mysql> select * from student;
+----+-----------+------+
| id | stuId     | name |
+----+-----------+------+
|  1 | 123456789 | jack |
|  3 |      NULL | tom  |
+----+-----------+------+

img.png

  • eq_ref唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。 常见于主键或唯一索引扫描img.png 对于b中的每一条数据,从a的主键索引中查找id和其相等的
  • ref,非唯一性索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问, 它返回所有匹配某个单独值的行,然而,它可能会找到多个 符合条件的行,所以他应该属于查找和扫描的混合体。( 查找是基于有序性的能利用二分,而扫描则是线性的
sql
mysql> create table `person` (
    ->  `id` int(32) not null auto_increment,
    ->  `firstName` varchar(30) default null,
    ->  `lastName` varchar(30) default null,
    ->  primary key(`id`),
    ->  index idx_name (firstName,lastName)
    -> ) engine=innodb auto_increment=1 default charset=utf8;

查询姓张的人: img.png

  • range,根据索引的有序性检索特定范围内的行,通常出现在between、<、>、in等范围检索中 img.png
  • index,在索引中扫描,只需读取索引数据。 由于复合索引idx_name是基于(firstName,lastName)的,这种索引只能保证在整体上是按定义时的第一列(即firstName)有序的,当firstName相同时,再按lastName排序,如果不只两列则以此类推。也就是说在根据lastName查找时是无法利用二分的,只能做全索引扫描。
  • all,全表扫描,需要从磁盘上读取表数据

备注:一般来说,得保证查询至少达到range级别,最好能达到ref

possible_keys(可能的索引选择)

MySQL可以利用以快速检索行的索引。

key ⭐(实际用到的索引)

MySQL执行时实际使用的索引。

key_len(实际索引长度)

  • 表示索引中每个元素最大字节数,可通过该列计算查询中使用的索引的长度(如何计算稍后详细结束)。

在不损失精确性的情况下,长度越短越好。

  • key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的。

ref(实际索引长度)

显示哪一列或常量被拿来与索引列进行比较以从表中检索行。

rows ⭐(预计要检查的行数)

根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数,值越小越好。

Extra 🌟(附加信息)

  • Using index:表示相应的select操作中使用了覆盖索引(Covering Index),避免访问了表的数据行(需要读磁盘),效率不错!
sql
mysql> explain select * from person order by firstName,lastName\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: person
   partitions: NULL
         type: index
possible_keys: NULL
          key: idx_name
      key_len: 186
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using index

使用\G代替;结尾可以使执行计划垂直显示。

  • Using temporary:使用了临时表保存中间结果。MySQL在对查询结果聚合时使用临时表。常见于排序 order by 和分组查询 group by
  • Using filesort:MySQL中无法利用索引完成的排序操作,只能在内存或者磁盘中进行排序,叫做Using filesort,使用了文件排序。 MySQL在对查询结果排序时使用文件排序
sql
mysql> insert into person(firstName,lastName) values('张','三');

mysql> insert into person(firstName,lastName) values('李','三');

mysql> insert into person(firstName,lastName) values('王','三');

mysql> insert into person(firstName,lastName) values('李','明');

mysql> select lastName,count(lastName) from person group by lastName;
+----------+-----------------+
| lastName | count(lastName) |
+----------+-----------------+
| 三       |               3 |
| 明       |               1 |
+----------+-----------------+

mysql> explain select lastName,count(lastName) from person group by lastName\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: person
   partitions: NULL
         type: index
possible_keys: idx_name
          key: idx_name
      key_len: 186
          ref: NULL
         rows: 4
     filtered: 100.00
        Extra: Using index; Using temporary; Using filesort

使用filesort的方式进行排序的记录非常多,那么这个过程是很耗费性能的,我们最好想办法将使用文件排序的执行方式改为索引进行排序。

  • 出现Using where,表明索引被用来执行索引键值的查找;如果没有同时出现Using where,表明索引用来读取数据而非执行查找动作。注意并不是不是所有带where语句的查询都会显示Using where。

索引覆盖:就是select的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件,换句话说查询列要被所建的索引覆盖。 如果要使用覆盖索引,一定要注意select列表中只取出需要的列,不可select *,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降。

  • Using where:查询使用到了where语句
  • Using index condition: 有些搜索条件中虽然出现了索引列,但却不能使用到索引,会显示Using index conditionimg.png

    因为key1 like '%b'不满足最左匹配原则,无法用到索引,显示Using index condition

  • Using join buffer:使用了连接缓存,在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL一般会为其分配一块名叫join buffer的内存块来加快查询速度,也就是我们所讲的基于块的嵌套循环算法。
  • Impossible where:where子句的值总是false,如
sql
select * from person where id=1 and id=2;

Released under the MIT License.