一,Explain

一条查询语句在经过MySQL查询优化器的各种基于成本和规则的优化会后生成一个所谓的执行计划,这个执行计划展示了接下来具体执行查询的方式,比如多表连接的顺序是什么,对于每个表采用什么访问方法来具体执行查询等等。MySQL为我们提供了EXPLAIN语句来帮助我们查看某个查询语句的具体执行计划,如果我们想看看某个查询的执行计划的话,可以在具体的查询语句前边加一个EXPLAIN,就像这样:

1.png

其实除了以SELECT开头的查询语句,其余的DELETEINSERTREPLACE以及UPDATE语句前边都可以加上EXPLAIN这个词儿,用来查看这些语句的执行计划,不过我们这里对SELECT语句更感兴趣,所以后边只会以SELECT语句为例来描述EXPLAIN语句的用法。我们先把EXPLAIN语句输出的各个列的作用先大致罗列一下:

列名 描述
id 在一个大的查询语句中每个SELECT关键字都对应一个唯一的id
select_type SELECT关键字对应的那个查询的类型
table 表名
partitions 匹配的分区信息
type 针对单表的访问方法
possible_keys 可能用到的索引
key 实际上使用的索引
key_len 实际使用到的索引长度
ref 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
rows 预估的需要读取的记录条数
filtered 某个表经过搜索条件过滤后剩余记录条数的百分比
Extra 一些额外的信息

我们前面创建过一张single_table表:

  1. CREATE TABLE single_table (
  2. id INT NOT NULL AUTO_INCREMENT,
  3. key1 VARCHAR(100),
  4. key2 INT,
  5. key3 VARCHAR(100),
  6. key_part1 VARCHAR(100),
  7. key_part2 VARCHAR(100),
  8. key_part3 VARCHAR(100),
  9. common_field VARCHAR(100),
  10. PRIMARY KEY (id),
  11. KEY idx_key1 (key1),
  12. UNIQUE KEY idx_key2 (key2),
  13. KEY idx_key3 (key3),
  14. KEY idx_key_part(key_part1, key_part2, key_part3)
  15. ) Engine=InnoDB CHARSET=utf8;

1.执行计划输出中各列详解

table

不论我们的查询语句有多复杂,里边儿包含了多少个表,到最后也是需要对每个表进行单表访问的,所以MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该表的表名。所以我们看一条比较简单的查询语句:

2.png

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

下边我们看一下一个连接查询的执行计划:

3.png

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

id

我们知道我们写的查询语句一般都以SELECT关键字开头,比较简单的查询语句里只有一个SELECT关键字,比如下边这个查询语句:

  1. SELECT * FROM s1 WHERE key1 = 'a';

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

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

但是下边两种情况下在一条查询语句中会出现多个SELECT关键字:

  • 查询中包含子查询的情况
    比如下边这个查询语句中就包含2个SELECT关键字:

    1. SELECT * FROM s1
    2. WHERE key1 IN (SELECT key3 FROM s2);
  • 查询中包含UNION语句的情况
    比如下边这个查询语句中也包含2个SELECT关键字:

    1. SELECT * FROM s1 UNION SELECT * FROM s2;

查询语句中每出现一个SELECT关键字,MySQL就会为它分配一个唯一的id值。这个id值就是EXPLAIN语句的第一个列,比如下边这个查询中只有一个SELECT关键字,所以EXPLAIN的结果中也就只有一条id列为1的记录:

4.png

对于连接查询来说,一个SELECT关键字后边的FROM子句中可以跟随多个表,所以在连接查询的执行计划中,每个表都会对应一条记录,但是这些记录的id值都是相同的,比如:

5.png

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

对于包含子查询的查询语句来说,就可能涉及多个SELECT关键字,所以在包含子查询的查询语句的执行计划中,每个SELECT关键字都会对应一个唯一的id值,比如这样:

6.png

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

查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询。所以如果我们想知道查询优化器对某个包含子查询的语句是否进行了重写,直接查看执行计划就好了,比如说:

7.png

虽然我们的查询语句是一个子查询,但是执行计划中s1s2表对应的记录的id值全部是1,这就表明了查询优化器将子查询转换为了连接查询。

对于包含UNION子句的查询语句来说,每个SELECT关键字对应一个id值也是没错的,不过还是有点儿特别,比方说下边这个查询:

8.png

这个语句的执行计划的第三条记录是个什么?为什么id值是NULL,而且table名也不大对?UNION子句会把多个查询的结果集合并起来并对结果集中的记录进行去重,怎么去重呢?MySQL使用的是内部的临时表。正如上边的查询计划中所示,UNION子句是为了把id1的查询和id2的查询的结果集合并起来并去重,所以在内部创建了一个临时表(就是执行计划第三条记录的table列的名称),idNULL表明这个临时表是为了合并两个查询的结果集而创建的。

UNION对比起来,UNION ALL就不需要为最终的结果集进行去重,它只是单纯的把多个查询的结果集中的记录合并成一个并返回给用户,所以也就不需要使用临时表。所以在包含UNION ALL子句的查询的执行计划中,就没有那个idNULL的记录,如下所示:

9.png

select_type

通过上边的内容我们知道,一条大的查询语句里边可以包含若干个SELECT关键字,每个SELECT关键字代表着一个小的查询语句,而每个SELECT关键字的FROM子句中都可以包含若干张表(这些表用来做连接查询),每一张表都对应着执行计划输出中的一条记录,对于在同一个SELECT关键字中的表来说,它们的id值是相同的。

MySQL为每一个SELECT关键字代表的小查询都定义了一个称之为select_type的属性,意思是我们只要知道了某个小查询的select_type属性,就知道了这个小查询在整个大查询中有什么作用。

名称 描述
SIMPLE Simple SELECT (not using UNION or subqueries)
PRIMARY Outermost SELECT
UNION Second or later SELECT statement in a UNION
UNION RESULT Result of a UNION
SUBQUERY First SELECT in subquery
DEPENDENT SUBQUERY First SELECT in subquery, dependent on outer query
DEPENDENT UNION Second or later SELECT statement in a UNION, dependent on outer query
DERIVED Derived table
MATERIALIZED Materialized subquery
UNCACHEABLE SUBQUERY A subquery for which the result cannot be cached and must be re-evaluated for each row of the outer query
UNCACHEABLE UNION The second or later select in a UNION that belongs to an uncacheable subquery (see UNCACHEABLE SUBQUERY)
  • SIMPLE
    查询语句中不包含UNION或者子查询的查询都算作是SIMPLE类型,比方说下边这个单表查询的select_type的值就是SIMPLE
    10.png
    当然,连接查询也算是SIMPLE类型,比如:
    11.png
  • PRIMARY
    对于包含UNIONUNION ALL或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的select_type值就是PRIMARY,比方说:
    12.png
    从结果中可以看到,最左边的小查询SELECT * FROM s1对应的是执行计划中的第一条记录,它的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,比如下边这个查询:
    13.png
    可以看到,外层查询的select_type就是PRIMARY,子查询的select_type就是SUBQUERY。由于select_type为SUBQUERY的子查询会被物化,所以只需要执行一遍。
  • DEPENDENT SUBQUERY
    如果包含子查询的查询语句不能够转为对应的semi-join的形式,并且该子查询是相关子查询,则该子查询的第一个SELECT关键字代表的那个查询的select_type就是DEPENDENT SUBQUERY,比如下边这个查询:
    14.png
    select_type为DEPENDENT SUBQUERY的查询可能会被执行多次。
  • DEPENDENT UNION
    在包含UNION或者UNION ALL的大查询中,如果各个小查询都依赖于外层查询的话,那除了最左边的那个小查询之外,其余的小查询的select_type的值就是DEPENDENT UNION。说的有些绕哈,比方说下边这个查询:
    15.png
    这个查询比较复杂,大查询里包含了一个子查询,子查询里又是由UNION连起来的两个小查询。从执行计划中可以看出来,SELECT key1 FROM s2 WHERE key1 = 'a'这个小查询由于是子查询中第一个查询,所以它的select_typeDEPENDENT SUBQUERY,而SELECT key1 FROM s1 WHERE key1 = 'b'这个查询的select_type就是DEPENDENT UNION
  • DERIVED
    对于采用物化的方式执行的包含派生表的查询,该派生表对应的子查询的select_type就是DERIVED,比方说下边这个查询:
    16.png
    从执行计划中可以看出,id2的记录就代表子查询的执行方式,它的select_typeDERIVED,说明该子查询是以物化的方式执行的。id1的记录代表外层查询,它的table列显示的是``,表示该查询是针对将派生表物化之后的表进行查询的。

    如果派生表可以通过和外层查询合并的方式执行的话,执行计划又是另一番景象。

  • MATERIALIZED
    当查询优化器在执行包含子查询的语句时,选择将子查询物化之后与外层查询进行连接查询时,该子查询对应的select_type属性就是MATERIALIZED,比如下边这个查询:
    17.png
    执行计划的第三条记录的id值为2,说明该条记录对应的是一个单表查询,从它的select_type值为MATERIALIZED可以看出,查询优化器是要把子查询先转换成物化表。然后看执行计划的前两条记录的id值都为1,说明这两条记录对应的表进行连接查询,需要注意的是第二条记录的table列的值是`,说明该表其实就是id2对应的子查询执行之后产生的物化表,然后将s1`和该物化表进行连接查询。

  • UNCACHEABLE SUBQUERY
    不常用
  • UNCACHEABLE UNION
    不常用

partitions

一般情况下我们的查询语句的执行计划的partitions列的值都是NULL

type

执行计划的一条记录就代表着MySQL对某个表的执行查询时的访问方法,其中的type列就表明了这个访问方法是什么,比方说下边这个查询:

18.png

可以看到type列的值是ref,表明MySQL即将使用ref访问方法来执行对s1表的查询。但是我们之前只分析过对使用InnoDB存储引擎的表进行单表访问的一些访问方法,完整的访问方法如下:systemconsteq_refreffulltextref_or_nullindex_mergeunique_subqueryindex_subqueryrangeindexALL。接下来我们详细看一下:

  • system
    当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory,那么对该表的访问方法就是system。比方说我们新建一个MyISAM表,并为其插入一条记录:
    然后我们看一下查询这个表的执行计划:
    19.png
    可以看到type列的值就是system了。
    ``` mysql> CREATE TABLE t(i int) Engine=MyISAM; Query OK, 0 rows affected (0.05 sec)

mysql> INSERT INTO t VALUES(1); Query OK, 1 row affected (0.01 sec)

  1. > 把表改成使用InnoDB存储引擎,执行计划的type列是ALL
  2. - `const`<br />当我们根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是`const`,比如:<br />![20.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280418708-2e9e505a-c985-45fa-8518-44c27fc83979.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u7438f4f7&margin=%5Bobject%20Object%5D&name=20.png&originHeight=133&originWidth=1220&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10419&status=done&style=none&taskId=ubcd36136-b889-4eac-aefb-c68f5da874a&title=)
  3. - `eq_ref`<br />在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是`eq_ref`,比方说:<br />![21.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280456161-049d60a4-d6da-46b2-91fe-9b2661fa1d36.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u0c6b2463&margin=%5Bobject%20Object%5D&name=21.png&originHeight=158&originWidth=1264&originalType=binary&ratio=1&rotation=0&showTitle=false&size=13498&status=done&style=none&taskId=ue5cce22c-1ad7-46d1-82c7-559db693de7&title=)<br />从执行计划的结果中可以看出,`MySQL`打算将`s1`作为驱动表,`s2`作为被驱动表,重点关注`s2`的访问方法是`eq_ref`,表明在访问`s2`表的时候可以通过主键的等值匹配来进行访问。
  4. - `ref`<br />当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是`ref`
  5. - `fulltext`<br />全文索引,意义不大。
  6. - `ref_or_null`<br />当对普通二级索引进行等值匹配查询,该索引列的值也可以是`NULL`值时,那么对该表的访问方法就可能是`ref_or_null`,比如说:<br />![22.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280463125-26170cb7-ba4b-4933-a004-8c346961b5da.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ufa9a4774&margin=%5Bobject%20Object%5D&name=22.png&originHeight=141&originWidth=1422&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12130&status=done&style=none&taskId=u6b7b7747-acae-4a15-adf5-d446fbbb6c0&title=)
  7. - `index_merge`<br />在某些场景下可以使用`Intersection``Union``Sort-Union`这三种索引合并的方式来执行查询,我们看一下执行计划中是怎么体现`MySQL`使用索引合并的方式来对某个表执行查询的:<br />![23.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280470378-3882e0be-f91d-44c0-a7b9-a979adc93085.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ue18487eb&margin=%5Bobject%20Object%5D&name=23.png&originHeight=130&originWidth=1806&originalType=binary&ratio=1&rotation=0&showTitle=false&size=18097&status=done&style=none&taskId=u3aaf69a0-df90-4b3e-a75d-7f161bc37f0&title=)<br />从执行计划的`type`列的值是`index_merge`就可以看出,`MySQL`打算使用索引合并的方式来执行对`s1`表的查询。
  8. - `unique_subquery`<br />类似于两表连接中被驱动表的`eq_ref`访问方法,`unique_subquery`是针对在一些包含`IN`子查询的查询语句中,如果查询优化器决定将`IN`子查询转换为`EXISTS`子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的`type`列的值就是`unique_subquery`,比如下边的这个查询语句:<br />![24.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280477014-ddc7ace3-5261-47a3-956c-3acce84f20dc.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u0d35569a&margin=%5Bobject%20Object%5D&name=24.png&originHeight=138&originWidth=1469&originalType=binary&ratio=1&rotation=0&showTitle=false&size=15149&status=done&style=none&taskId=ub0da4c73-db17-427c-b87b-e059de96b1b&title=)<br />可以看到执行计划的第二条记录的`type`值就是`unique_subquery`,说明在执行子查询时会使用到`id`列的索引。
  9. - `index_subquery`<br />`index_subquery``unique_subquery`类似,只不过访问子查询中的表时使用的是普通的索引,比如这样:<br />![25.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280483868-ff7a1815-8c05-4a2f-94e7-79b880cea954.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ue020d52f&margin=%5Bobject%20Object%5D&name=25.png&originHeight=148&originWidth=1457&originalType=binary&ratio=1&rotation=0&showTitle=false&size=16363&status=done&style=none&taskId=u2782b719-e96b-4afa-bb75-8eb977622af&title=)
  10. - `range`<br />如果使用索引获取某些`范围区间`的记录,那么就可能使用到`range`访问方法,比如下边的这个查询:<br />![26.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280490568-7cef7a7c-7acf-4be1-9c08-5b6da950acfc.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u08f05ea8&margin=%5Bobject%20Object%5D&name=26.png&originHeight=143&originWidth=1371&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11837&status=done&style=none&taskId=u99904bc8-68ad-4658-a5fc-b5851815429&title=)<br />或者:<br />![27.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280496929-e1318302-934b-4bda-b7a4-8a858c278403.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u14fae300&margin=%5Bobject%20Object%5D&name=27.png&originHeight=117&originWidth=1355&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10119&status=done&style=none&taskId=u13e335fa-04ea-4d0e-861b-045a44df8c6&title=)
  11. - `index`<br />当我们可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是`index`,比如这样:<br />![28.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280502811-c5c73a14-cb70-410e-9997-eb5296fe7ead.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u9d0bfa53&margin=%5Bobject%20Object%5D&name=28.png&originHeight=129&originWidth=1445&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11338&status=done&style=none&taskId=u8ea5271b-21e8-4dfe-b21f-f9b7cce2e5a&title=)<br />上述查询中的搜索列表中只有`key_part2`一个列,而且搜索条件中也只有`key_part3`一个列,这两个列又恰好包含在`idx_key_part`这个索引中,可是搜索条件`key_part3`不能直接使用该索引进行`ref`或者`range`方式的访问,只能扫描整个`idx_key_part`索引的记录,所以查询计划的`type`列的值就是`index`。
  12. > 对于使用InnoDB存储引擎的表来说,二级索引的记录只包含索引列和主键列的值,而聚簇索引中包含用户定义的全部列以及一些隐藏列,所以扫描二级索引的代价比直接全表扫描,也就是扫描聚簇索引的代价更低一些。
  13. - `ALL`<br />最熟悉的全表扫描直接看例子:<br />![29.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280509611-e1909546-52e6-44eb-a2ad-bc9422cf1521.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u8e1ce447&margin=%5Bobject%20Object%5D&name=29.png&originHeight=136&originWidth=1172&originalType=binary&ratio=1&rotation=0&showTitle=false&size=9873&status=done&style=none&taskId=ucadc9702-0049-4b37-bcc0-a6e180d38b3&title=)
  14. 一般来说,这些访问方法按照我们介绍它们的顺序性能依次变差。其中除了`All`这个访问方法外,其余的访问方法都能用到索引,除了`index_merge`访问方法外,其余的访问方法都最多只能用到一个索引。
  15. <a name="86e54cb5"></a>
  16. ### possible_keys和key
  17. `EXPLAIN`语句输出的执行计划中,`possible_keys`列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些,`key`列表示实际用到的索引有哪些,比方说下边这个查询:
  18. ![30.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280516466-778cee1a-3912-45ce-b8b0-961fde6131b2.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u8c4c0e4c&margin=%5Bobject%20Object%5D&name=30.png&originHeight=135&originWidth=1311&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11553&status=done&style=none&taskId=u20dfc39d-6030-42fa-8480-2d27b8ab68a&title=)
  19. 上述执行计划的`possible_keys`列的值是`idx_key1,idx_key3`,表示该查询可能使用到`idx_key1,idx_key3`两个索引,然后`key`列的值是`idx_key3`,表示经过查询优化器计算使用不同索引的成本后,最后决定使用`idx_key3`来执行查询比较划算。
  20. 不过有一点比较特别,就是在使用`index`访问方法来查询某个表时,`possible_keys`列是空的,而`key`列展示的是实际使用到的索引,比如这样:
  21. ![31.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280523465-50db2fcf-3aab-459d-bd56-9fe94cfef62d.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u709cacbf&margin=%5Bobject%20Object%5D&name=31.png&originHeight=133&originWidth=1456&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11806&status=done&style=none&taskId=u7d4f3316-a2df-450a-82f1-e9dc395c5d1&title=)
  22. 另外需要注意的一点是,possible_keys列中的值并不是越多越好,可能使用的索引越多,查询优化器计算查询成本时就得花费更长时间,所以如果可以的话,尽量删除那些用不到的索引。
  23. <a name="key_len"></a>
  24. ### key_len
  25. `key_len`列表示当优化器决定使用某个索引执行查询时,该索引记录的最大长度,它是由这三个部分构成的:
  26. 1. 对于使用固定长度类型的索引列来说,它实际占用的存储空间的最大长度就是该固定值,对于指定字符集的变长类型的索引列来说,比如某个索引列的类型是`VARCHAR(100)`,使用的字符集是`utf8`,那么该列实际占用的最大存储空间就是`100 × 3 = 300`个字节。
  27. 2. 如果该索引列可以存储`NULL`值,则`key_len`比不可以存储`NULL`值时多1个字节。
  28. 3. 对于变长字段来说,都会有2个字节的空间来存储该变长列的实际长度。
  29. 比如下边这个查询:
  30. ![32.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280530011-7d5e65ca-3e73-4ce5-9cd7-30e30fca150d.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u37395cf6&margin=%5Bobject%20Object%5D&name=32.png&originHeight=126&originWidth=1194&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10064&status=done&style=none&taskId=u620f6e76-7540-4994-b49b-8ecb5d6c62a&title=)
  31. 由于`id`列的类型是`INT`,并且不可以存储`NULL`值,所以在使用该列的索引时`key_len`大小就是`4`。当索引列可以存储`NULL`值时,比如:
  32. ![33.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280536833-34a98995-bbb1-4f5b-9c65-cd883418fbd2.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u91c8ee45&margin=%5Bobject%20Object%5D&name=33.png&originHeight=132&originWidth=1203&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10328&status=done&style=none&taskId=uf7c83065-c654-47e4-886d-3e1fc98b6e8&title=)
  33. 可以看到`key_len`列就变成了`5`,比使用`id`列的索引时多了`1`
  34. 对于可变长度的索引列来说,比如下边这个查询:
  35. ![34.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280547140-fea8d226-3465-4e13-b93d-0e74882cf422.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=uc8b9f40b&margin=%5Bobject%20Object%5D&name=34.png&originHeight=137&originWidth=1206&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10837&status=done&style=none&taskId=ubc579fb7-242e-4242-844b-50635088a6d&title=)
  36. 由于`key1`列的类型是`VARCHAR(100)`,所以该列实际最多占用的存储空间就是`300`字节,又因为该列允许存储`NULL`值,所以`key_len`需要加`1`,又因为该列是可变长度列,所以`key_len`需要加`2`,所以最后`ken_len`的值就是`303`
  37. 这里需要强调的一点是,执行计划的生成是在`MySQL server`层中的功能,并不是针对具体某个存储引擎的功能,`MySQL`在执行计划中输出`key_len`列主要是为了让我们区分某个使用联合索引的查询具体用了几个索引列,而不是为了准确的说明针对某个具体存储引擎存储变长字段的实际长度占用的空间到底是占用1个字节还是2个字节。比方说下边这个使用到联合索引`idx_key_part`的查询:
  38. ![35.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280574627-423abda2-7282-4686-850d-158ee23ceef0.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ue80e815d&margin=%5Bobject%20Object%5D&name=35.png&originHeight=135&originWidth=1251&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11390&status=done&style=none&taskId=ua1103849-bb46-4d1e-bfe7-d43baa19357&title=)
  39. 我们可以从执行计划的`key_len`列中看到值是`303`,这意味着`MySQL`在执行上述查询中只能用到`idx_key_part`索引的一个索引列,而下边这个查询:
  40. ![36.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280584178-4d046ed5-9399-46e8-acf8-c4dba1e88a44.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ufe6bdb41&margin=%5Bobject%20Object%5D&name=36.png&originHeight=148&originWidth=1310&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11956&status=done&style=none&taskId=u5a6a7a9e-56f9-4f90-b3e3-3ccce6aeeb1&title=)
  41. 这个查询的执行计划的`ken_len`列的值是`606`,说明执行这个查询的时候可以用到联合索引`idx_key_part`的两个索引列。
  42. <a name="ref"></a>
  43. ### ref
  44. 当使用索引列等值匹配的条件去执行查询时,也就是在访问方法是`const``eq_ref``ref``ref_or_null``unique_subquery``index_subquery`其中之一时,`ref`列展示的就是与索引列作等值匹配的东东是个啥,比如只是一个常数或者是某个列。
  45. ![37.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280639189-65511db0-6781-43a2-a189-da56d4a39d06.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u3e0a564f&margin=%5Bobject%20Object%5D&name=37.png&originHeight=137&originWidth=1205&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10813&status=done&style=none&taskId=ubf40913d-f7f9-48b6-a1bc-22be2439bce&title=)
  46. 可以看到`ref`列的值是`const`,表明在使用`idx_key1`索引执行查询时,与`key1`列作等值匹配的对象是一个常数,当然有时候更复杂一点:
  47. ![38.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280647362-c56ac11e-c810-49b1-b6ef-74142632a2f3.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u2efc95bd&margin=%5Bobject%20Object%5D&name=38.png&originHeight=151&originWidth=1270&originalType=binary&ratio=1&rotation=0&showTitle=false&size=13438&status=done&style=none&taskId=u78dba58c-d2a6-4a38-8342-d8829ff9213&title=)
  48. 可以看到对被驱动表`s2`的访问方法是`eq_ref`,而对应的`ref`列的值是`yhd.s1.id`,这说明在对被驱动表进行访问时会用到`PRIMARY`索引,也就是聚簇索引与一个列进行等值匹配的条件,于`s2`表的`id`作等值匹配的对象就是`yhd.s1.id`列(注意这里把数据库名也写出来了)。
  49. 有的时候与索引列进行等值匹配的对象是一个函数,比方说下边这个查询:
  50. ![39.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280654511-c4a8f217-0578-4a32-b78c-1b61a2160364.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u11c13c47&margin=%5Bobject%20Object%5D&name=39.png&originHeight=152&originWidth=1372&originalType=binary&ratio=1&rotation=0&showTitle=false&size=14314&status=done&style=none&taskId=u9c0c9cba-8fec-41c7-82d0-9469e60fcc2&title=)
  51. 我们看执行计划的第二条记录,可以看到对`s2`表采用`ref`访问方法执行查询,然后在查询计划的`ref`列里输出的是`func`,说明与`s2`表的`key1`列进行等值匹配的对象是一个函数。
  52. <a name="rows"></a>
  53. ### rows
  54. 如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的`rows`列就代表预计需要扫描的行数,如果使用索引来执行查询时,执行计划的`rows`列就代表预计扫描的索引记录行数。比如下边这个查询:
  55. ![40.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280662371-b8621d39-7404-4335-8b6f-6ac3bf020927.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ubdf57c66&margin=%5Bobject%20Object%5D&name=40.png&originHeight=144&originWidth=1362&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11525&status=done&style=none&taskId=u502bf400-c9d0-4d98-a90c-01d6e656856&title=)
  56. 我们看到执行计划的`rows`列的值是`1`,这意味着查询优化器在经过分析使用`idx_key1`进行查询的成本之后,觉得满足`key1 > 'z'`这个条件的记录只有`1`条。
  57. <a name="filtered"></a>
  58. ### filtered
  59. 之前在分析连接查询的成本时提出过一个`condition filtering`的概念,就是`MySQL`在计算驱动表扇出时采用的一个策略:
  60. - 如果使用的是全表扫描的方式执行的单表查询,那么计算驱动表扇出时需要估计出满足搜索条件的记录到底有多少条。
  61. - 如果使用的是索引执行的单表扫描,那么计算驱动表扇出的时候需要估计出满足除使用到对应索引的搜索条件外的其他搜索条件的记录有多少条。
  62. 比方说下边这个查询:
  63. ![41.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280695851-4f237801-1121-4cce-935d-8d124f691b30.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u08ebf4a7&margin=%5Bobject%20Object%5D&name=41.png&originHeight=139&originWidth=1507&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12747&status=done&style=none&taskId=uef9d60d8-c348-4d80-a2f2-29ebce41900&title=)
  64. 从执行计划的`key`列中可以看出来,该查询使用`idx_key1`索引来执行查询,从`rows`列可以看出满足`key1 > 'z'`的记录有`1`条。执行计划的`filtered`列就代表查询优化器预测在这`1`条记录中,有多少条记录满足其余的搜索条件,也就是`common_field = 'a'`这个条件的百分比。此处`filtered`列的值是`10.00`,说明查询优化器预测在`1`条记录中有`10.00%`的记录满足`common_field = 'a'`这个条件。
  65. 对于单表查询来说,这个`filtered`列的值没什么意义,我们更关注在连接查询中驱动表对应的执行计划记录的`filtered`值,比方说下边这个查询:
  66. ![42.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280702717-5e6276df-2775-4dd9-9068-168632a6e891.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ua075a642&margin=%5Bobject%20Object%5D&name=42.png&originHeight=159&originWidth=1359&originalType=binary&ratio=1&rotation=0&showTitle=false&size=15418&status=done&style=none&taskId=u2ce577e9-11ca-4213-8c24-e76cc816cc0&title=)
  67. 从执行计划中可以看出来,查询优化器打算把`s1`当作驱动表,`s2`当作被驱动表。我们可以看到驱动表`s1`表的执行计划的`rows`列为`997219` `filtered`列为`10.00`,这意味着驱动表`s1`的扇出值就是`997219 × 10.00% = 99721.9`,这说明还要对被驱动表执行大约`99721.9`次查询。
  68. <a name="Extra"></a>
  69. ### Extra
  70. 顾名思义,`Extra`列是用来说明一些额外信息的,我们可以通过这些额外信息来更准确的理解`MySQL`到底将如何执行给定的查询语句。
  71. - `No tables used`<br />当查询语句的没有`FROM`子句时将会提示该额外信息,比如:<br />![43.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280709963-b0a3875e-f1e9-4112-928c-5a2509c69a1c.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u0af38ec6&margin=%5Bobject%20Object%5D&name=43.png&originHeight=137&originWidth=1246&originalType=binary&ratio=1&rotation=0&showTitle=false&size=8351&status=done&style=none&taskId=ub8c0c8c7-6dd1-436a-a457-2a8f3fd0a43&title=)
  72. - `Impossible WHERE`<br />查询语句的`WHERE`子句永远为`FALSE`时将会提示该额外信息,比方说:<br />![44.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280716070-b375d8db-fab7-411f-b6c3-fdb1cb8a2b68.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u62a56c70&margin=%5Bobject%20Object%5D&name=44.png&originHeight=140&originWidth=1270&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10429&status=done&style=none&taskId=u3592d4c4-8b55-4c36-ab4c-19bfcb10f2a&title=)
  73. - `No matching min/max row`<br />当查询列表处有`MIN`或者`MAX`聚集函数,但是并没有符合`WHERE`子句中的搜索条件的记录时,将会提示该额外信息,比方说:<br />![45.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280722423-8f00c35d-e352-4e4f-9ad0-06344f87d7be.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=uae911a2d&margin=%5Bobject%20Object%5D&name=45.png&originHeight=133&originWidth=1318&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10632&status=done&style=none&taskId=uc25dd0ec-cd40-4efa-b32b-289e0c6a293&title=)
  74. - `Using index`<br />当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用索引覆盖的情况下,在`Extra`列将会提示该额外信息。比方说下边这个查询中只需要用到`idx_key1`而不需要回表操作:<br />![46.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280729248-e8d19929-5057-4e2b-92b5-4f0815d42951.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=uca03b967&margin=%5Bobject%20Object%5D&name=46.png&originHeight=137&originWidth=1271&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10748&status=done&style=none&taskId=u4ef056f1-92c6-4c14-8947-39aa551837d&title=)
  75. - `Using index condition`<br />有些搜索条件中虽然出现了索引列,但却不能使用到索引,比如下边这个查询: <br />其中的`key1 > 'z'`可以使用到索引,但是`key1 LIKE '%a'`却无法使用到索引,在以前版本的`MySQL`中,是按照下边步骤来执行这个查询的:

SELECT * FROM s1 WHERE key1 > ‘z’ AND key1 LIKE ‘%a’;

  1. - 先根据`key1 > 'z'`这个条件,从二级索引`idx_key1`中获取到对应的二级索引记录。
  2. - 根据上一步骤得到的二级索引记录中的主键值进行回表,找到完整的用户记录再检测该记录是否符合`key1 LIKE '%a'`这个条件,将符合条件的记录加入到最后的结果集。
  3. 但是虽然`key1 LIKE '%a'`不能组成范围区间参与`range`访问方法的执行,但这个条件毕竟只涉及到了`key1`列,所以`MySQL`把上边的步骤改进了一下:
  4. - 先根据`key1 > 'z'`这个条件,定位到二级索引`idx_key1`中对应的二级索引记录。
  5. - 对于指定的二级索引记录,先不着急回表,而是先检测一下该记录是否满足`key1 LIKE '%a'`这个条件,如果这个条件不满足,则该二级索引记录压根儿就没必要回表。
  6. - 对于满足`key1 LIKE '%a'`这个条件的二级索引记录执行回表操作。
  7. 我们说回表操作其实是一个随机`IO`,比较耗时,所以上述修改虽然只改进了一点点,但是可以省去好多回表操作的成本。`MySQL`把他们的这个改进称之为`索引条件下推`(英文名:`Index Condition Pushdown`)。<br />如果在查询语句的执行过程中将要使用`索引条件下推`这个特性,在`Extra`列中将会显示`Using index condition`,比如这样:<br />![47.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280739039-b867ce7b-7772-4c60-ad85-de88f84dfc95.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u5f7b5a5f&margin=%5Bobject%20Object%5D&name=47.png&originHeight=150&originWidth=1355&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12096&status=done&style=none&taskId=u108c9eb7-bb19-4820-a2c3-34bdae80479&title=)
  8. - `Using where`<br />当我们使用全表扫描来执行对某个表的查询,并且该语句的`WHERE`子句中有针对该表的搜索条件时,在`Extra`列中会提示上述额外信息。比如下边这个查询:<br />![48.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280745116-15cf2e92-4bc6-4896-a114-a08fd4f149f1.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ue69355bd&margin=%5Bobject%20Object%5D&name=48.png&originHeight=142&originWidth=1251&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11283&status=done&style=none&taskId=u5bca1059-5304-4173-a759-fe3383de92f&title=)<br />当使用索引访问来执行对某个表的查询,并且该语句的`WHERE`子句中有除了该索引包含的列之外的其他搜索条件时,在`Extra`列中也会提示上述额外信息。比如下边这个查询虽然使用`idx_key1`索引执行查询,但是搜索条件中除了包含`key1`的搜索条件`key1 = 'a'`,还有包含`common_field`的搜索条件,所以`Extra`列会显示`Using where`的提示:<br />![49.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280750980-0af4ffa5-7801-40ed-b434-60cf04e53c39.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u6328631b&margin=%5Bobject%20Object%5D&name=49.png&originHeight=139&originWidth=1270&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11868&status=done&style=none&taskId=u78e29b4d-4d4a-49e1-85b3-0469f52cbfc&title=)
  9. - `Using join buffer (Block Nested Loop)`<br />在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,`MySQL`一般会为其分配一块名叫`join buffer`的内存块来加快查询速度,也就是我们所讲的`基于块的嵌套循环算法`,比如下边这个查询语句:<br />![50.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280756935-cbffc56a-f033-486c-bc9e-2a4b45faffc3.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u66ccbdd4&margin=%5Bobject%20Object%5D&name=50.png&originHeight=197&originWidth=1615&originalType=binary&ratio=1&rotation=0&showTitle=false&size=16002&status=done&style=none&taskId=u33176016-e036-4aa1-a1e2-fb9469510e0&title=)<br />可以在对`s2`表的执行计划的`Extra`列显示了两个提示:
  10. - `Using join buffer (Block Nested Loop)`:这是因为对表`s2`的访问不能有效利用索引,只好退而求其次,使用`join buffer`来减少对`s2`表的访问次数,从而提高性能。
  11. - `Using where`:可以看到查询语句中有一个`s1.common_field = s2.common_field`条件,因为`s1`是驱动表,`s2`是被驱动表,所以在访问`s2`表时,`s1.common_field`的值已经确定下来了,所以实际上查询`s2`表的条件就是`s2.common_field = 一个常数`,所以提示了`Using where`额外信息。
  12. - `Not exists`<br />当我们使用左(外)连接时,如果`WHERE`子句中包含要求被驱动表的某个列等于`NULL`值的搜索条件,而且那个列又是不允许存储`NULL`值的,那么在该表的执行计划的`Extra`列就会提示`Not exists`额外信息,比如这样:<br />![51.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280765248-adc53a64-08e5-4047-9282-dedf49c0ba53.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ud66d86bd&margin=%5Bobject%20Object%5D&name=51.png&originHeight=168&originWidth=1735&originalType=binary&ratio=1&rotation=0&showTitle=false&size=20626&status=done&style=none&taskId=ue194205d-192b-4293-96ba-cbbb11125a2&title=)<br />上述查询中`s1`表是驱动表,`s2`表是被驱动表,`s2.id`列是不允许存储`NULL`值的,而`WHERE`子句中又包含`s2.id IS NULL`的搜索条件,这意味着必定是驱动表的记录在被驱动表中找不到匹配`ON`子句条件的记录才会把该驱动表的记录加入到最终的结果集,所以对于某条驱动表中的记录来说,如果能在被驱动表中找到1条符合`ON`子句条件的记录,那么该驱动表的记录就不会被加入到最终的结果集,也就是说我们没有必要到被驱动表中找到全部符合ON子句条件的记录,这样可以稍微节省一点性能。
  13. > 右(外)连接可以被转换为左(外)连接,所以就不提右(外)连接的情况了。
  14. - `Using intersect(...)``Using union(...)``Using sort_union(...)`<br />如果执行计划的`Extra`列出现了`Using intersect(...)`提示,说明准备使用`Intersect`索引合并的方式执行查询,括号中的`...`表示需要进行索引合并的索引名称;如果出现了`Using union(...)`提示,说明准备使用`Union`索引合并的方式执行查询;出现了`Using sort_union(...)`提示,说明准备使用`Sort-Union`索引合并的方式执行查询。比如这个查询的执行计划:<br />![52.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280772463-3a90c8d1-6545-4018-b8d7-5025f232d7ee.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ueda5799e&margin=%5Bobject%20Object%5D&name=52.png&originHeight=143&originWidth=1303&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11849&status=done&style=none&taskId=u894c398e-c275-41a7-8968-5e3e310bd43&title=)<br />其中`Extra`列就显示了`Using intersect(idx_key3,idx_key1)`,表明`MySQL`即将使用`idx_key3`和`idx_key1`这两个索引进行`Intersect`索引合并的方式执行查询。
  15. - `Zero limit`<br />当我们的`LIMIT`子句的参数为`0`时,表示不打算从表中读出任何记录,将会提示该额外信息,比如这样:<br />![53.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280778887-9254ccb9-15d2-4dca-ae74-454146fcd5bc.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u805e1066&margin=%5Bobject%20Object%5D&name=53.png&originHeight=153&originWidth=1206&originalType=binary&ratio=1&rotation=0&showTitle=false&size=9929&status=done&style=none&taskId=ud78823f5-06ba-40ce-b3b7-d471c6eb90f&title=)
  16. - `Using filesort`<br />有一些情况下对结果集中的记录进行排序是可以使用到索引的,比如下边这个查询:<br />![54.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280785452-754d9c90-0fa1-49ec-889c-dc8cf142399b.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u231ab400&margin=%5Bobject%20Object%5D&name=54.png&originHeight=154&originWidth=1214&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11063&status=done&style=none&taskId=u9983daa3-8d15-47c4-aeea-15b73d0dcc1&title=)<br />这个查询语句可以利用`idx_key1`索引直接取出`key1`列的10条记录,然后再进行回表操作就好了。但是很多情况下排序操作无法使用到索引,只能在内存中(记录较少的时候)或者磁盘中(记录较多的时候)进行排序,`MySQL`把这种在内存中或者磁盘上进行排序的方式统称为文件排序(英文名:`filesort`)。如果某个查询需要使用文件排序的方式执行查询,就会在执行计划的`Extra`列中显示`Using filesort`提示,比如这样:<br />![55.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280791344-a71710e6-a915-4788-bc09-688eec1a399b.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u7aa08f31&margin=%5Bobject%20Object%5D&name=55.png&originHeight=148&originWidth=1290&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11614&status=done&style=none&taskId=u5f2f6bce-bad5-4b54-8664-8d144260c1a&title=)<br />需要注意的是,如果查询中需要使用`filesort`的方式进行排序的记录非常多,那么这个过程是很耗费性能的,我们最好想办法将使用`文件排序`的执行方式改为使用索引进行排序。
  17. - `Using temporary`<br />在许多查询的执行过程中,`MySQL`可能会借助临时表来完成一些功能,比如去重、排序之类的,比如我们在执行许多包含`DISTINCT``GROUP BY``UNION`等子句的查询过程中,如果不能有效利用索引来完成查询,`MySQL`很有可能寻求通过建立内部的临时表来执行查询。如果查询中使用到了内部的临时表,在执行计划的`Extra`列将会显示`Using temporary`提示,比方说这样:<br />![56.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280797689-75d2cb3d-3866-480c-bf4e-8a69e479536f.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u286716ac&margin=%5Bobject%20Object%5D&name=56.png&originHeight=151&originWidth=1270&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11377&status=done&style=none&taskId=u4ce60cde-24ba-4663-aadd-a4e68b73f0d&title=)<br />再比如:<br />![57.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280803837-7d795aa0-fd17-4d87-9ab0-07c295f5e7c9.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ue7b2c60b&margin=%5Bobject%20Object%5D&name=57.png&originHeight=149&originWidth=1425&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12625&status=done&style=none&taskId=ub9d1b398-3b4f-4f73-a1ee-342d85bf749&title=)<br />不知道大家注意到没有,上述执行计划的`Extra`列不仅仅包含`Using temporary`提示,还包含`Using filesort`提示,可是我们的查询语句中明明没有写`ORDER BY`子句呀?这是因为`MySQL`会在包含`GROUP BY`子句的查询中默认添加上`ORDER BY`子句,也就是说上述查询其实和下边这个查询等价:

EXPLAIN SELECT common_field, COUNT(*) AS amount FROM s1 GROUP BY common_field ORDER BY common_field;

  1. 如果我们并不想为包含GROUP BY子句的查询进行排序,需要我们显式的写上ORDER BY NULL,就像这样:
  2. ![58.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280846631-90d86605-c8b9-4548-b10d-b44be5d9335f.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u822663e5&margin=%5Bobject%20Object%5D&name=58.png&originHeight=150&originWidth=1271&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12514&status=done&style=none&taskId=u61a5fe59-aa4c-4df4-b86a-5d5c7d5c8fe&title=)
  3. 这回执行计划中就没有Using filesort的提示了,也就意味着执行查询时可以省去对记录进行文件排序的成本了。
  4. 另外,执行计划中出现Using temporary并不是一个好的征兆,因为建立与维护临时表要付出很大成本的,所以我们最好能使用索引来替代掉使用临时表,比方说下边这个包含GROUP BY子句的查询就不需要使用临时表:
  5. ![59.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280856295-6e72f854-6076-48fe-9766-4b8dd70a434c.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=uaf5517ea&margin=%5Bobject%20Object%5D&name=59.png&originHeight=136&originWidth=1279&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11837&status=done&style=none&taskId=ucb545596-9525-4ce1-ab15-e96493d37f6&title=)
  6. ExtraUsing index的提示里我们可以看出,上述查询只需要扫描idx_key1索引就可以搞定了,不再需要临时表了。
  7. - `Start temporary, End temporary`<br />查询优化器会优先尝试将`IN`子查询转换成`semi-join`,而`semi-join`又有好多种执行策略,当执行策略为`DuplicateWeedout`时,也就是通过建立临时表来实现为外层查询中的记录进行去重操作时,驱动表查询执行计划的`Extra`列将显示`Start temporary`提示,被驱动表查询执行计划的`Extra`列将显示`End temporary`提示,就是这样:<br />![60.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280870260-d8109158-d388-4665-87a3-fbc3393cb1e6.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u40bf656b&margin=%5Bobject%20Object%5D&name=60.png&originHeight=180&originWidth=1443&originalType=binary&ratio=1&rotation=0&showTitle=false&size=18905&status=done&style=none&taskId=u60106558-b64a-4cad-8c4c-7e89266b735&title=)
  8. - `LooseScan`<br />在将`In`子查询转为`semi-join`时,如果采用的是`LooseScan`执行策略,则在驱动表执行计划的`Extra`列就是显示`LooseScan`提示,比如这样:<br />![61.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280878656-f9849a88-d738-4e61-92fe-71e8fb4b8097.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ud020c3be&margin=%5Bobject%20Object%5D&name=61.png&originHeight=182&originWidth=1553&originalType=binary&ratio=1&rotation=0&showTitle=false&size=18884&status=done&style=none&taskId=u48e16db1-823b-481d-93b1-641580ba363&title=)
  9. - `FirstMatch(tbl_name)`<br />在将`In`子查询转为`semi-join`时,如果采用的是`FirstMatch`执行策略,则在被驱动表执行计划的`Extra`列就是显示`FirstMatch(tbl_name)`提示,比如这样:<br />![62.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280886946-4cee061e-db5d-429b-9f77-887af2159190.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ue6d1ea12&margin=%5Bobject%20Object%5D&name=62.png&originHeight=171&originWidth=1613&originalType=binary&ratio=1&rotation=0&showTitle=false&size=17585&status=done&style=none&taskId=ue1333208-0342-496e-bc90-8215638090b&title=)
  10. <a name="7b4d2c84"></a>
  11. ## 2.Json格式的执行计划
  12. 我们上边介绍的`EXPLAIN`语句输出中缺少了一个衡量执行计划好坏的重要属性 —— 成本。不过`MySQL`贴心的为我们提供了一种查看某个执行计划花费的成本的方式:
  13. - `EXPLAIN`单词和真正的查询语句中间加上`FORMAT=JSON`
  14. 这样我们就可以得到一个`json`格式的执行计划,里边儿包含该计划花费的成本,比如这样:

mysql> EXPLAIN FORMAT=JSON SELECT FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2 WHERE s1.common_field = ‘a’\G ** 1. row *

EXPLAIN: { “query_block”: { “select_id”: 1, # 整个查询语句只有1个SELECT关键字,该关键字对应的id号为1 “cost_info”: { “query_cost”: “3197.16” # 整个查询的执行成本预计为3197.16 }, “nested_loop”: [ # 几个表之间采用嵌套循环连接算法执行

  1. # 以下是参与嵌套循环连接算法的各个表的信息
  2. {
  3. "table": {
  4. "table_name": "s1", # s1表是驱动表
  5. "access_type": "ALL", # 访问方法为ALL,意味着使用全表扫描访问
  6. "possible_keys": [ # 可能使用的索引
  7. "idx_key1"
  8. ],
  9. "rows_examined_per_scan": 9688, # 查询一次s1表大致需要扫描9688条记录
  10. "rows_produced_per_join": 968, # 驱动表s1的扇出是968
  11. "filtered": "10.00", # condition filtering代表的百分比
  12. "cost_info": {
  13. "read_cost": "1840.84", # 稍后解释
  14. "eval_cost": "193.76", # 稍后解释
  15. "prefix_cost": "2034.60", # 单次查询s1表总共的成本
  16. "data_read_per_join": "1M" # 读取的数据量
  17. },
  18. "used_columns": [ # 执行查询中涉及到的列
  19. "id",
  20. "key1",
  21. "key2",
  22. "key3",
  23. "key_part1",
  24. "key_part2",
  25. "key_part3",
  26. "common_field"
  27. ],
  28. # 对s1表访问时针对单表查询的条件
  29. "attached_condition": "((`xiaohaizi`.`s1`.`common_field` = 'a') and (`xiaohaizi`.`s1`.`key1` is not null))"
  30. }
  31. },
  32. {
  33. "table": {
  34. "table_name": "s2", # s2表是被驱动表
  35. "access_type": "ref", # 访问方法为ref,意味着使用索引等值匹配的方式访问
  36. "possible_keys": [ # 可能使用的索引
  37. "idx_key2"
  38. ],
  39. "key": "idx_key2", # 实际使用的索引
  40. "used_key_parts": [ # 使用到的索引列
  41. "key2"
  42. ],
  43. "key_length": "5", # key_len
  44. "ref": [ # 与key2列进行等值匹配的对象
  45. "xiaohaizi.s1.key1"
  46. ],
  47. "rows_examined_per_scan": 1, # 查询一次s2表大致需要扫描1条记录
  48. "rows_produced_per_join": 968, # 被驱动表s2的扇出是968(由于后边没有多余的表进行连接,所以这个值也没啥用)
  49. "filtered": "100.00", # condition filtering代表的百分比
  50. # s2表使用索引进行查询的搜索条件
  51. "index_condition": "(`xiaohaizi`.`s1`.`key1` = `xiaohaizi`.`s2`.`key2`)",
  52. "cost_info": {
  53. "read_cost": "968.80", # 稍后解释
  54. "eval_cost": "193.76", # 稍后解释
  55. "prefix_cost": "3197.16", # 单次查询s1、多次查询s2表总共的成本
  56. "data_read_per_join": "1M" # 读取的数据量
  57. },
  58. "used_columns": [ # 执行查询中涉及到的列
  59. "id",
  60. "key1",
  61. "key2",
  62. "key3",
  63. "key_part1",
  64. "key_part2",
  65. "key_part3",
  66. "common_field"
  67. ]
  68. }
  69. }
  70. ]

} } 1 row in set, 2 warnings (0.00 sec)

  1. `"cost_info"`里边的成本是怎么计算出来的?先看`s1`表的`"cost_info"`部分:

“cost_info”: { “read_cost”: “1840.84”, “eval_cost”: “193.76”, “prefix_cost”: “2034.60”, “data_read_per_join”: “1M” }

  1. - `read_cost`是由下边这两部分组成的:
  2. - `IO`成本
  3. - 检测`rows × (1 - filter)`条记录的`CPU`成本
  4. > rowsfilter都是我们前边介绍执行计划的输出列,在JSON格式的执行计划中,rows相当于rows_examined_per_scanfiltered名称不变。
  5. - `eval_cost`是这样计算的:<br />检测 `rows × filter`条记录的成本。
  6. - `prefix_cost`就是单独查询`s1`表的成本,也就是:<br />`read_cost + eval_cost`
  7. - `data_read_per_join`表示在此次查询中需要读取的数据量,我们就不多唠叨这个了。
  8. > 其实没必要关注MySQL为啥使用这么古怪的方式计算出read_costeval_cost,关注prefix_cost是查询s1表的成本就好了。
  9. 对于`s2`表的`"cost_info"`部分是这样的:

“cost_info”: { “read_cost”: “968.80”, “eval_cost”: “193.76”, “prefix_cost”: “3197.16”, “data_read_per_join”: “1M” }

  1. 由于`s2`表是被驱动表,所以可能被读取多次,这里的`read_cost``eval_cost`是访问多次`s2`表后累加起来的值,主要关注里边儿的`prefix_cost`的值代表的是整个连接查询预计的成本,也就是单次查询`s1`表和多次查询`s2`表后的成本的和,也就是:

968.80 + 193.76 + 2034.60 = 3197.16

  1. <a name="e8324445"></a>
  2. ## 3.Extented EXPLAIN
  3. 在我们使用`EXPLAIN`语句查看了某个查询的执行计划后,紧接着还可以使用`SHOW WARNINGS`语句查看与这个查询的执行计划有关的一些扩展信息,比如这样:
  4. ![63.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280900493-ef323458-9ed7-4213-a1bd-1b1a606384aa.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=uf7584cec&margin=%5Bobject%20Object%5D&name=63.png&originHeight=308&originWidth=1560&originalType=binary&ratio=1&rotation=0&showTitle=false&size=27815&status=done&style=none&taskId=ub7ecd933-32ca-458a-8f52-efe2fb29519&title=)
  5. 可以看到`SHOW WARNINGS`展示出来的信息有三个字段,分别是`Level`、`Code`、`Message`。我们最常见的就是`Code`为`1003`的信息,当`Code`值为`1003`时,`Message`字段展示的信息类似于查询优化器将我们的查询语句重写后的语句。比如我们上边的查询本来是一个左(外)连接查询,但是有一个`s2.common_field IS NOT NULL`的条件,着就会导致查询优化器把左(外)连接查询优化为内连接查询,从`SHOW WARNINGS`的`Message`字段也可以看出来,原本的`LEFT JOIN`已经变成了`JOIN`。
  6. 我们说`Message`字段展示的信息类似于查询优化器将我们的查询语句重写后的语句,并不是等价于,也就是说`Message`字段展示的信息并不是标准的查询语句,在很多情况下并不能直接运行,它只能作为帮助我们理解查`MySQL`将如何执行查询语句的一个参考依据而已。
  7. <a name="7bdd2956"></a>
  8. # 二,optimizer trace
  9. 对于`MySQL 5.6`以及之前的版本来说,查询优化器就像是一个黑盒子一样,只能通过`EXPLAIN`语句查看到最后优化器决定使用的执行计划,却无法知道它为什么做这个决策。
  10. 在`MySQL 5.6`以及之后的版本中,`MySQL`提出了一个`optimizer trace`的功能,这个功能可以让我们方便的查看优化器生成执行计划的整个过程,这个功能的开启与关闭由系统变量`optimizer_trace`决定,我们看一下:

mysql> SHOW VARIABLES LIKE ‘optimizer_trace’; +————————-+—————————————+ | Variable_name | Value | +————————-+—————————————+ | optimizer_trace | enabled=off,one_line=off | +————————-+—————————————+ 1 row in set (0.02 sec)

  1. 可以看到`enabled`值为`off`,表明这个功能默认是关闭的。
  2. > one_line的值是控制输出格式的,如果为on那么所有输出都将在一行中展示,不适合人阅读,所以我们就保持其默认值为off吧。
  3. 如果想打开这个功能,必须首先把`enabled`的值改为`on`,就像这样:

mysql> SET optimizer_trace=”enabled=on”; Query OK, 0 rows affected (0.00 sec)

  1. 然后我们就可以输入我们想要查看优化过程的查询语句,当该查询语句执行完成后,就可以到`information_schema`数据库下的`OPTIMIZER_TRACE`表中查看完整的优化过程。这个`OPTIMIZER_TRACE`表有4个列,分别是:
  2. - `QUERY`:表示我们的查询语句。
  3. - `TRACE`:表示优化过程的JSON格式文本。
  4. - `MISSING_BYTES_BEYOND_MAX_MEM_SIZE`:由于优化过程可能会输出很多,如果超过某个限制时,多余的文本将不会被显示,这个字段展示了被忽略的文本字节数。
  5. - `INSUFFICIENT_PRIVILEGES`:表示是否没有权限查看优化过程,默认值是0,只有某些特殊情况下才会是`1`,我们暂时不关心这个字段的值。
  6. 完整的使用`optimizer trace`功能的步骤总结如下:

1. 打开optimizer trace功能 (默认情况下它是关闭的):

SET optimizer_trace=”enabled=on”;

2. 这里输入查询语句

SELECT …;

3. 从OPTIMIZER_TRACE表中查看上一个查询的优化过程

SELECT * FROM information_schema.OPTIMIZER_TRACE;

4. 可能还要观察其他语句执行的优化过程,重复上边的第2、3步

5. 当停止查看语句的优化过程时,把optimizer trace功能关闭

SET optimizer_trace=”enabled=off”;

  1. 现在我们有一个搜索条件比较多的查询语句,它的执行计划如下:
  2. ![64.png](https://cdn.nlark.com/yuque/0/2021/png/12610368/1640280913355-faf51846-5a6c-457f-8c09-ef9a61f1e7f0.png#clientId=udee86ba6-ae37-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ua3d01893&margin=%5Bobject%20Object%5D&name=64.png&originHeight=133&originWidth=1608&originalType=binary&ratio=1&rotation=0&showTitle=false&size=13872&status=done&style=none&taskId=u3f87397a-3e3c-45de-8b02-c7efbc04957&title=)
  3. 可以看到该查询可能使用到的索引有3个,那么为什么优化器最终选择了`idx_key2`而不选择其他的索引或者直接全表扫描呢?这时候就可以通过`otpimzer trace`功能来查看优化器的具体工作过程:

SET optimizer_trace=”enabled=on”;

SELECT * FROM s1 WHERE key1 > ‘z’ AND key2 < 1000000 AND key3 IN (‘a’, ‘b’, ‘c’) AND common_field = ‘abc’;

SELECT * FROM information_schema.OPTIMIZER_TRACE\G

  1. 直接看一下通过查询`OPTIMIZER_TRACE`表得到的输出:

* 1. row *

分析的查询语句是什么

QUERY: SELECT * FROM s1 WHERE key1 > ‘z’ AND key2 < 1000000 AND key3 IN (‘a’, ‘b’, ‘c’) AND common_field = ‘abc’

优化的具体过程

TRACE: { “steps”: [ { “join_preparation”: { # prepare阶段 “select#”: 1, “steps”: [ { “IN_uses_bisection”: true }, { “expanded_query”: “/ select#1 / select s1.id AS id,s1.key1 AS key1,s1.key2 AS key2,s1.key3 AS key3,s1.key_part1 AS key_part1,s1.key_part2 AS key_part2,s1.key_part3 AS key_part3,s1.common_field AS common_field from s1 where ((s1.key1 > ‘z’) and (s1.key2 < 1000000) and (s1.key3 in (‘a’,’b’,’c’)) and (s1.common_field = ‘abc’))” } ] / steps / } / join_preparation / }, { “join_optimization”: { # optimize阶段 “select#”: 1, “steps”: [ { “condition_processing”: { # 处理搜索条件 “condition”: “WHERE”,

  1. # 原始搜索条件
  2. "original_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))",
  3. "steps": [
  4. {
  5. # 等值传递转换
  6. "transformation": "equality_propagation",
  7. "resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
  8. },
  9. {
  10. # 常量传递转换
  11. "transformation": "constant_propagation",
  12. "resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
  13. },
  14. {
  15. # 去除没用的条件
  16. "transformation": "trivial_condition_removal",
  17. "resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
  18. }
  19. ] /* steps */
  20. } /* condition_processing */
  21. },
  22. {
  23. # 替换虚拟生成列
  24. "substitute_generated_columns": {
  25. } /* substitute_generated_columns */
  26. },
  27. {
  28. # 表的依赖信息
  29. "table_dependencies": [
  30. {
  31. "table": "`s1`",
  32. "row_may_be_null": false,
  33. "map_bit": 0,
  34. "depends_on_map_bits": [
  35. ] /* depends_on_map_bits */
  36. }
  37. ] /* table_dependencies */
  38. },
  39. {
  40. "ref_optimizer_key_uses": [
  41. ] /* ref_optimizer_key_uses */
  42. },
  43. {
  44. # 预估不同单表访问方法的访问成本
  45. "rows_estimation": [
  46. {
  47. "table": "`s1`",
  48. "range_analysis": {
  49. "table_scan": { # 全表扫描的行数以及成本
  50. "rows": 9688,
  51. "cost": 2036.7
  52. } /* table_scan */,
  53. # 分析可能使用的索引
  54. "potential_range_indexes": [
  55. {
  56. "index": "PRIMARY", # 主键不可用
  57. "usable": false,
  58. "cause": "not_applicable"
  59. },
  60. {
  61. "index": "idx_key2", # idx_key2可能被使用
  62. "usable": true,
  63. "key_parts": [
  64. "key2"
  65. ] /* key_parts */
  66. },
  67. {
  68. "index": "idx_key1", # idx_key1可能被使用
  69. "usable": true,
  70. "key_parts": [
  71. "key1",
  72. "id"
  73. ] /* key_parts */
  74. },
  75. {
  76. "index": "idx_key3", # idx_key3可能被使用
  77. "usable": true,
  78. "key_parts": [
  79. "key3",
  80. "id"
  81. ] /* key_parts */
  82. },
  83. {
  84. "index": "idx_key_part", # idx_keypart不可用
  85. "usable": false,
  86. "cause": "not_applicable"
  87. }
  88. ] /* potential_range_indexes */,
  89. "setup_range_conditions": [
  90. ] /* setup_range_conditions */,
  91. "group_index_range": {
  92. "chosen": false,
  93. "cause": "not_group_by_or_distinct"
  94. } /* group_index_range */,
  95. # 分析各种可能使用的索引的成本
  96. "analyzing_range_alternatives": {
  97. "range_scan_alternatives": [
  98. {
  99. # 使用idx_key2的成本分析
  100. "index": "idx_key2",
  101. # 使用idx_key2的范围区间
  102. "ranges": [
  103. "NULL < key2 < 1000000"
  104. ] /* ranges */,
  105. "index_dives_for_eq_ranges": true, # 是否使用index dive
  106. "rowid_ordered": false, # 使用该索引获取的记录是否按照主键排序
  107. "using_mrr": false, # 是否使用mrr
  108. "index_only": false, # 是否是索引覆盖访问
  109. "rows": 12, # 使用该索引获取的记录条数
  110. "cost": 15.41, # 使用该索引的成本
  111. "chosen": true # 是否选择该索引
  112. },
  113. {
  114. # 使用idx_key1的成本分析
  115. "index": "idx_key1",
  116. # 使用idx_key1的范围区间
  117. "ranges": [
  118. "z < key1"
  119. ] /* ranges */,
  120. "index_dives_for_eq_ranges": true, # 同上
  121. "rowid_ordered": false, # 同上
  122. "using_mrr": false, # 同上
  123. "index_only": false, # 同上
  124. "rows": 266, # 同上
  125. "cost": 320.21, # 同上
  126. "chosen": false, # 同上
  127. "cause": "cost" # 因为成本太大所以不选择该索引
  128. },
  129. {
  130. # 使用idx_key3的成本分析
  131. "index": "idx_key3",
  132. # 使用idx_key3的范围区间
  133. "ranges": [
  134. "a <= key3 <= a",
  135. "b <= key3 <= b",
  136. "c <= key3 <= c"
  137. ] /* ranges */,
  138. "index_dives_for_eq_ranges": true, # 同上
  139. "rowid_ordered": false, # 同上
  140. "using_mrr": false, # 同上
  141. "index_only": false, # 同上
  142. "rows": 21, # 同上
  143. "cost": 28.21, # 同上
  144. "chosen": false, # 同上
  145. "cause": "cost" # 同上
  146. }
  147. ] /* range_scan_alternatives */,
  148. # 分析使用索引合并的成本
  149. "analyzing_roworder_intersect": {
  150. "usable": false,
  151. "cause": "too_few_roworder_scans"
  152. } /* analyzing_roworder_intersect */
  153. } /* analyzing_range_alternatives */,
  154. # 对于上述单表查询s1最优的访问方法
  155. "chosen_range_access_summary": {
  156. "range_access_plan": {
  157. "type": "range_scan",
  158. "index": "idx_key2",
  159. "rows": 12,
  160. "ranges": [
  161. "NULL < key2 < 1000000"
  162. ] /* ranges */
  163. } /* range_access_plan */,
  164. "rows_for_plan": 12,
  165. "cost_for_plan": 15.41,
  166. "chosen": true
  167. } /* chosen_range_access_summary */
  168. } /* range_analysis */
  169. }
  170. ] /* rows_estimation */
  171. },
  172. {
  173. # 分析各种可能的执行计划
  174. #(对多表查询这可能有很多种不同的方案,单表查询的方案上边已经分析过了,直接选取idx_key2就好)
  175. "considered_execution_plans": [
  176. {
  177. "plan_prefix": [
  178. ] /* plan_prefix */,
  179. "table": "`s1`",
  180. "best_access_path": {
  181. "considered_access_paths": [
  182. {
  183. "rows_to_scan": 12,
  184. "access_type": "range",
  185. "range_details": {
  186. "used_index": "idx_key2"
  187. } /* range_details */,
  188. "resulting_rows": 12,
  189. "cost": 17.81,
  190. "chosen": true
  191. }
  192. ] /* considered_access_paths */
  193. } /* best_access_path */,
  194. "condition_filtering_pct": 100,
  195. "rows_for_plan": 12,
  196. "cost_for_plan": 17.81,
  197. "chosen": true
  198. }
  199. ] /* considered_execution_plans */
  200. },
  201. {
  202. # 尝试给查询添加一些其他的查询条件
  203. "attaching_conditions_to_tables": {
  204. "original_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))",
  205. "attached_conditions_computation": [
  206. ] /* attached_conditions_computation */,
  207. "attached_conditions_summary": [
  208. {
  209. "table": "`s1`",
  210. "attached": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
  211. }
  212. ] /* attached_conditions_summary */
  213. } /* attaching_conditions_to_tables */
  214. },
  215. {
  216. # 再稍稍的改进一下执行计划
  217. "refine_plan": [
  218. {
  219. "table": "`s1`",
  220. "pushed_index_condition": "(`s1`.`key2` < 1000000)",
  221. "table_condition_attached": "((`s1`.`key1` > 'z') and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
  222. }
  223. ] /* refine_plan */
  224. }
  225. ] /* steps */
  226. } /* join_optimization */
  227. },
  228. {
  229. "join_execution": { # execute阶段
  230. "select#": 1,
  231. "steps": [
  232. ] /* steps */
  233. } /* join_execution */
  234. }

] / steps / }

因优化过程文本太多而丢弃的文本字节大小,值为0时表示并没有丢弃

MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0

权限字段

INSUFFICIENT_PRIVILEGES: 0

1 row in set (0.00 sec) ```

这只是优化器执行过程中的一小部分,MySQL可能会在之后的版本中添加更多的优化过程信息。不过杂乱之中其实还是蛮有规律的,优化过程大致分为了三个阶段:

  • prepare阶段
  • optimize阶段
  • execute阶段

我们所说的基于成本的优化主要集中在optimize阶段,对于单表查询来说,我们主要关注optimize阶段的"rows_estimation"这个过程,这个过程深入分析了对单表查询的各种执行方案的成本;对于多表连接查询来说,我们更多需要关注"considered_execution_plans"这个过程,这个过程里会写明各种不同的连接方式所对应的成本。反正优化器最终会选择成本最低的那种方案来作为最终的执行计划,也就是我们使用EXPLAIN语句所展现出的那种方案。

如果对使用EXPLAIN语句展示出的对某个查询的执行计划很不理解,可以尝试使用optimizer trace功能来详细了解每一种执行方案对应的成本。