在MySQL中,查看SQL语句的执行计划是一个常用且重要的性能优化手段,执行计划可以告诉开发者MySQL的查询语句是如何使用索引,连接表的顺序,以及估计的行数等。要查看执行计划,可以使用EXPLAIN语句

执行计划字段

image

执行计划各列的含义

列名

描述

id

在一个大的查询语句中每个SELECT关键字都对应一个唯一的id

select_type

SELECT关键字对应的那个查询的类型

table

表名

partitions

匹配的分区信息

type

针对单表的访问方法

possible_keys

可能用到的索引

key

实际上使用的索引

key_len

实际使用到的索引长度

ref

当使用索引列等值查询时,与索引列进行等值匹配的对象信息

rows

预估的需要读取的记录条数

filtered

某个表经过搜索条件过滤后剩余记录条数的百分比

Extra

一些额外的信息

详解

我们假设有两张表 s1 和 s2,表结构如下,并且两个表里都有10000条记录。

CREATE TABLE single_table (
    id INT NOT NULL AUTO_INCREMENT,
    key1 VARCHAR(100),
    key2 INT,
    key3 VARCHAR(100),
    key_part1 VARCHAR(100),
    key_part2 VARCHAR(100),
    key_part3 VARCHAR(100),
    common_field VARCHAR(100),
    PRIMARY KEY (id),
    KEY idx_key1 (key1),
    UNIQUE KEY idx_key2 (key2),
    KEY idx_key3 (key3),
    KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;

table

无论我们的查询语句多复杂,里边包含多少个表,到最后也是需要对每个表进行单表访问的,所以explain语句输出的每条记录都对应了某个单表的访问方法,该条记录的table列代表该表的表明。

我们先看下单表查询:这个查询语句只涉及对s1表的单表查询,所以EXPLAIN输出中只有一条记录,其中的table列的值是s1,表明这条记录是用来说明对s1表的单表访问方法的。 image

下边我们看一下一个连接查询的执行计划:可以看到这个连接查询的执行计划中有两条记录,这两条记录的table列分别是s1和s2,这两条记录用来分别说明对s1表和s2表的访问方法是什么。 image

id

大多数情况下,查询语句都是以SELECT关键字开头的,比较简单的查询语句里只有一个SELECT关键字,比如下边这个查询语句:

SELECT * FROM s1 WHERE key1 = 'a';

稍微复杂的连接查询中,也只有一个SELECT关键字,如:

SELECT * FROM s1 INNER JOIN s2
    ON s1.key1 = s2.key1
    WHERE s1.common_field = 'a';

但是有一些情况下,一条查询语句中会出现多个SELECT关键字,如:

  • 包含子查询的情况

SELECT * FROM s1 
    WHERE key1 IN (SELECT key3 FROM s2);
  • 包含UNION语句的情况

SELECT * FROM s1  UNION SELECT * FROM s2;

查询语句中每出现一个SELECT关键字,MySQL就会为它分配一个唯一的id值,这个id值就是EXPLAIN结果的第一个列。

连接查询

如下是一个连接查询语句,语句中包含了一个SELECT关键字,分配了一个idimage

上述连接查询中参与连接的s1s2表分别对应一条记录,但是这两条记录对应的id值都是1。这里需要大家记住的是,在连接查询的执行计划中,每个表都会对应一条记录,这些记录的id列的值是相同的,出现在前边的表表示驱动表,出现在后边的表表示被驱动表。所以从上边的EXPLAIN输出中我们可以看出,查询优化器准备让s1表作为驱动表,让s2表作为被驱动表来执行查询。

子查询

如下是一个包含子查询的语句,语句中包含了两个SELECT关键字,分配了两个id

image

从输出结果中我们可以看到,s1表在外层查询中,外层查询有一个独立的SELECT关键字,所以第一条记录的id值就是1s2表在子查询中,子查询有一个独立的SELECT关键字,所以第二条记录的id值就是2

需要注意的是,查询优化器可能会对子查询的语句进行重新,从而转换为连接查询。所以,如果我们想知道查询优化器对某个子查询的语句是否进行了重新,可以根据执行计划来判断。

UNION查询

对于包含UNION子句的查询语句来说,每个SELECT关键字对应一个id值也是没错的,但是UNION查询会有一个去重的过程,因此生成了一个id为NULLEXPLAIN记录。如果是UNION ALL则不会有这条记录。

image

去重的过程:MySQL使用的是内部的临时表。正如上边的查询计划中所示,UNION子句是为了把id为1的查询和id为2的查询的结果集合并起来并去重,所以在内部创建了一个名为<union1, 2>的临时表(就是执行计划第三条记录的table列的名称),id为NULL表明这个临时表是为了合并两个查询的结果集而创建的。

select_type

select_type代表着这个小查询在整个大查询中扮演了什么角色,select_type的取值有:SIMPLE,PRIMARY,UNION,UNION RESULT,SUBQUERY,DEPENDENT SUBQUERY,DEPENDENT UNION,DERIVED,MATERIALIZED,UNCACHEABLE SUBQUERY,UNCACHEABLE UNION

不要被吓到~一个一个来

  • SIMPLE

查询语句中不包含UNION或者子查询的查询都算作是SIMPLE类型,连接查询也算是SIMPLE类型

  • PRIMARY

对于包含UNIONUNION ALL或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的select_type值就是PRIMARY

  • UNION

对于包含UNION或者UNION ALL的大查询来说,它是由几个小查询组成的,其中除了最左边的那个小查询以外,其余的小查询的select_type值就是UNION

  • UNION RESULT

MySQL选择使用临时表来完成UNION查询的去重工作,针对该临时表的查询的select_type就是UNION RESULT

  • SUBQUERY

如果包含子查询的查询语句不能够转为对应的semi-join的形式,并且该子查询是不相关子查询,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,该子查询的第一个SELECT关键字代表的那个查询的select_type就是SUBQUERY,比如下边这个查询:

image

  • 物化:将子查询结果集中的记录保存到临时表的过程称之为物化,可以简单理解为为了优化查询(具体优化过程改日再聊)

  • 相关子查询:如果子查询的执行需要依赖于外层查询的值,我们就可以把这个子查询称之为相关子查询

    SELECT * FROM s1 WHERE s1.key1 IN (SELECT key2 FROM s2 WHERE s2.key1 = s1.key2);

  • 不相关子查询:如果子查询可以单独运行出结果,而不依赖于外层查询的值,我们就可以把这个子查询称之为不相关子查询

  • DEPENDENT SUBQUERY

如果包含子查询的查询语句不能够转为对应的semi-join的形式,并且该子查询是相关子查询,则该子查询的第一个SELECT关键字代表的那个查询的select_type就是DEPENDENT SUBQUERY

  • DEPENDENT UNION

在包含UNION或者UNION ALL的大查询中,如果各个小查询都依赖于外层查询的话,那除了最左边的那个小查询之外,其余的小查询的select_type的值就是DEPENDENT UNION

  • DERIVED

对于采用物化的方式执行的包含派生表的查询,该派生表对应的子查询的select_type就是DERIVED

  • MATERIALIZED

当查询优化器在执行包含子查询的语句时,选择将子查询物化之后与外层查询进行连接查询时,该子查询对应的select_type属性就是MATERIALIZED

type

type表示MySQL对某个表的执行查询时的访问方法,其中type就是表明了具体的访问方法。完整的访问方法有:system,const,eq_ref,ref,fulltext,ref_or_null,index_merge,unique_subquery,index_subquery,range,index,ALL

  • system

当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory,那么对该表的访问方法就是system

  • const

当我们根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是const

  • eq_ref

在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是eq_ref

image

从执行计划的结果中可以看出,MySQL打算将s1作为驱动表,s2作为被驱动表,重点关注s2的访问方法是eq_ref,表明在访问s2表的时候可以通过主键的等值匹配来进行访问。

  • ref

当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref

  • fulltext

全文检索,跳过

  • ref_or_null

当对普通二级索引进行等值匹配查询,该索引列的值也可以是NULL值时,那么对该表的访问方法就可能是ref_or_null

  • index_merge

一般情况下对于某个表的查询只能使用到一个索引,但单表访问方法在某些场景下可以使用IntersectionUnionSort-Union这三种索引合并的方式来执行查询。

  • Intersection:交集,这里表示一个查询可以使用多个二级索引,将从多个二级索引中查询到的结果取交集

  • Union和Sort-Union:并集,表示使用多个二级索引查询后,结果取并集,具体差别日后再说

  • unique_subquery

类似于两表连接中被驱动表的eq_ref访问方法,unique_subquery是针对在一些包含IN子查询的查询语句中,如果查询优化器决定将IN子查询转换为EXISTS子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的type列的值就是unique_subquery

  • index_subquery

index_subqueryunique_subquery类似,只不过访问子查询中的表时使用的是普通的索引

  • range

如果使用索引获取某些范围区间的记录,那么就可能使用到range访问方法,如:

EXPLAIN SELECT * FROM s1 WHERE key1 IN ('a', 'b', 'c');
或
EXPLAIN SELECT * FROM s1 WHERE key1 > 'a' AND key1 < 'b';
  • index

我们可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是index。(正常的走索引的查询,还需要回表

  • ALL

全表扫描

possible_keys和key

EXPLAIN语句输出的执行计划中,possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些,key列表示实际用到的索引有哪些

有一点比较特别,就是在使用index访问方法来查询某个表时,possible_keys列是空的,而key列展示的是实际使用到的索引

key_len

key_len列表示当优化器决定使用某个索引执行查询时,该索引记录的最大长度,它是由这三个部分构成的:

  • 对于使用固定长度类型的索引列来说,它实际占用的存储空间的最大长度就是该固定值,对于指定字符集的变长类型的索引列来说,比如某个索引列的类型是VARCHAR(100),使用的字符集是utf8,那么该列实际占用的最大存储空间就是100 × 3 = 300个字节。

  • 如果该索引列可以存储NULL值,则key_len比不可以存储NULL值时多1个字节。

  • 对于变长字段来说,都会有2个字节的空间来存储该变长列的实际长度。

ref

当使用索引列等值匹配的条件去执行查询时,也就是在访问方法是consteq_refrefref_or_nullunique_subqueryindex_subquery其中之一时,ref列展示的就是与索引列作等值匹配的东东是个啥。比如只是一个常数const或者是某个列如: 库名.s1.id 或者是一个函数 func

rows

如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的rows列就代表预计需要扫描的行数,如果使用索引来执行查询时,执行计划的rows列就代表预计扫描的索引记录行数。

filtered

跳过