EXPLAIN FORMAT=json和EXPLAIN ANALYZE查询计划解读

EXPLAIN 

  • 名词:

QEP: Query Execution Plan,查询执行计划。

  • 语法:
EXPLAIN [explain_type] {explainable_stmt }
explain_type: 
{ EXTENDED | PARTITIONS | FORMAT = format_name}

format_name:
{ TRADITIONAL | JSON}
explainable_stmt:
{ SELECT statement | DELETE statement | INSERT statement | REPLACE statement | UPDATE statement }

通常EXPLAIN用于获取QEP,而DESCRIBE、DESC用于获取表结构信息。

explain_type中,EXTENDED和PARTITIONS 可不写,默认会输出这两列的信息。如:

mysql> EXPLAIN SELECT s1.key1, s2.key1 FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.common_field IS NOT NULL;
+----+-------------+-------+------------+------+---------------+----------+---------+-------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key      | key_len | ref               | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+----------+---------+-------------------+------+----------+-------------+
|  1 | SIMPLE      | s2    | NULL       | ALL  | idx_key1      | NULL     | NULL    | NULL              | 9954 |    90.00 | Using where |
|  1 | SIMPLE      | s1    | NULL       | ref  | idx_key1      | idx_key1 | 303     | xiaohaizi.s2.key1 |    1 |   100.00 | Using index |
+----+-------------+-------+------------+------+---------------+----------+---------+-------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

 

在我们使用EXPLAIN语句查看了某个查询的执行计划后,紧接着还可以使用SHOW WARNINGS语句查看与这个查询的执行计划有关的一些扩展信息,比如这样:

mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `xiaohaizi`.`s1`.`key1` AS `key1`,`xiaohaizi`.`s2`.`key1` AS `key1` from `xiaohaizi`.`s1` join `xiaohaizi`.`s2` where ((`xiaohaizi`.`s1`.`key1` = `xiaohaizi`.`s2`.`key1`) and (`xiaohaizi`.`s2`.`common_field` is not null))
1 row in set (0.00 sec)

可以看到SHOW WARNINGS展示出来的信息有三个字段,分别是LevelCodeMessage。我们最常见的就是Code1003的信息,当Code值为1003时,Message字段展示的信息类似于查询优化器将我们的查询语句重写后的语句。比如我们上边的查询本来是一个左(外)连接查询,但是有一个s2.common_field IS NOT NULL的条件,着就会导致查询优化器把左(外)连接查询优化为内连接查询,从SHOW WARNINGSMessage字段也可以看出来,原本的LEFT JOIN已经变成了JOIN

但是要注意,我们说Message字段展示的信息类似于查询优化器将我们的查询语句重写后的语句,并不是等价于,也就是说Message字段展示的信息并不是标准的查询语句,在很多情况下并不能直接拿到黑框框中运行,它只能作为帮助我们理解查MySQL将如何执行查询语句的一个参考依据而已。

 

EXPLAIN语句输出的各个列的作用先大致罗列一下:

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

select_type

名称 描述
SIMPLE 简单查询(不包含子查询或UNION)
PRIMARY 最外层查询(UNION语句中最左边的查询)
UNION UNION语句中第二或更后面的查询
UNION RESULT 依赖外部查询(临时表)的UNION中第二或更后面的查询
SUBQUERY UNION语句的结果集
DEPENDENT UNION 依赖外部查询的子查询中的第一个查询
DERIVED 查询的派生表(在FROM从句中的子查询)
MATERIALIZED 物化子查询
UNCACHEABLE SUBQUERY 无法缓存结果的子查询,并且必须为外部查询的每一行重新计算
UNCACHEABLE UNION

属于无法缓存的子查询的UNION的第二或更后面的查询

type

访问方法按照顺序性能依次变差:systemconsteq_refreffulltextref_or_nullindex_mergeunique_subqueryindex_subqueryrangeindexALL。其中除了All这个访问方法外,其余的访问方法都能用到索引,除了index_merge访问方法外,其余的访问方法都最多只能用到一个索引。

名称 描述
system 表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory
const 根据主键或者唯一二级索引列与常数进行等值匹配
eq_ref 在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较)
ref 通过普通的二级索引列与常量进行等值匹配时来查询某个表
fulltext 全文索引
ref_or_null 对普通二级索引进行等值匹配查询,该索引列的值也可以是NULL值时
index_merge 多个索引合并查询
unique_subquery 针对包含IN子查询的查询语句中,查询优化器将IN子查询转换为EXISTS子查询,且子查询可以使用到主键进行等值匹配
index_subquery unique_subquery类似,只不过访问子查询中的表时使用的是普通的索引
range 使用索引获取某些范围区间的记录
index 可以使用索引覆盖,但需要扫描全部的索引记录
ALL 全表扫描

 

更详细内容查看《详解Mysql执行计划explain》:https://blog.csdn.net/weixin_38004638/article/details/103300300

 

 

EXPLAIN FORMAT=json

EXPLAIN [explain_type] {explainable_stmt }
explain_type: 
{ EXTENDED | PARTITIONS | FORMAT = format_name}

format_name:
{ TRADITIONAL | JSON}

format_name中TRADITIONAL表示输出格式跟上面一样的表格输出,默认此输出格式。JSON表示输出为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": [    # 几个表之间采用嵌套循环连接算法执行
    
    # 以下是参与嵌套循环连接算法的各个表的信息
      {
        "table": {
          "table_name": "s1",   # s1表是驱动表
          "access_type": "ALL",     # 访问方法为ALL,意味着使用全表扫描访问
          "possible_keys": [    # 可能使用的索引
            "idx_key1"
          ],
          "rows_examined_per_scan": 9688,   # 查询一次s1表大致需要扫描9688条记录
          "rows_produced_per_join": 968,    # 驱动表s1的扇出是968
          "filtered": "10.00",  # condition filtering代表的百分比
          "cost_info": {
            "read_cost": "1840.84",     # 稍后解释
            "eval_cost": "193.76",      # 稍后解释
            "prefix_cost": "2034.60",   # 单次查询s1表总共的成本
            "data_read_per_join": "1M"  # 读取的数据量
          },
          "used_columns": [     # 执行查询中涉及到的列
            "id",
            "key1",
            "key2",
            "key3",
            "key_part1",
            "key_part2",
            "key_part3",
            "common_field"
          ],
          
          # 对s1表访问时针对单表查询的条件
          "attached_condition": "((`xiaohaizi`.`s1`.`common_field` = 'a') and (`xiaohaizi`.`s1`.`key1` is not null))"
        }
      },
      {
        "table": {
          "table_name": "s2",   # s2表是被驱动表
          "access_type": "ref",     # 访问方法为ref,意味着使用索引等值匹配的方式访问
          "possible_keys": [    # 可能使用的索引
            "idx_key2"
          ],
          "key": "idx_key2",    # 实际使用的索引
          "used_key_parts": [   # 使用到的索引列
            "key2"
          ],
          "key_length": "5",    # key_len
          "ref": [      # 与key2列进行等值匹配的对象
            "xiaohaizi.s1.key1"
          ],
          "rows_examined_per_scan": 1,  # 查询一次s2表大致需要扫描1条记录
          "rows_produced_per_join": 968,    # 被驱动表s2的扇出是968(由于后边没有多余的表进行连接,所以这个值也没啥用)
          "filtered": "100.00",     # condition filtering代表的百分比
          
          # s2表使用索引进行查询的搜索条件
          "index_condition": "(`xiaohaizi`.`s1`.`key1` = `xiaohaizi`.`s2`.`key2`)",
          "cost_info": {
            "read_cost": "968.80",      # 稍后解释
            "eval_cost": "193.76",      # 稍后解释
            "prefix_cost": "3197.16",   # 单次查询s1、多次查询s2表总共的成本
            "data_read_per_join": "1M"  # 读取的数据量
          },
          "used_columns": [     # 执行查询中涉及到的列
            "id",
            "key1",
            "key2",
            "key3",
            "key_part1",
            "key_part2",
            "key_part3",
            "common_field"
          ]
        }
      }
    ]
  }
}
1 row in set, 2 warnings (0.00 sec)

 

cost_info"是怎么计算出来的?先看s1表的"cost_info"部分:

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

read_cost是由下边这两部分组成的:

  1. IO成本
  2. 检测rows × (1 - filter)条记录的CPU成本
rows和filter都是我们前边介绍执行计划的输出列,在JSON格式的执行计划中,rows相当于rows_examined_per_scan,filtered名称不变。

eval_cost是这样计算的:检测 rows × filter条记录的成本。

prefix_cost就是单独查询s1表的成本,也就是:read_cost + eval_cost,这个是重点关注的值

data_read_per_join表示在此次查询中需要读取的数据量

 

对于s2表的"cost_info"部分是这样的:

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

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

968.80 + 193.76 + 2034.60 = 3197.16

 

EXPLAIN ANALYZE

MySQL 8.0.18 刚刚发布,它包含一个全新的功能 EXPLAIN ANALYZE,用来分析和理解查询如何执行。EXPLAIN ANALYZE 是一个用于查询的分析工具,它向用户显示 MySQL 在查询上花费的时间以及原因。它将产生查询计划,并对其进行检测和执行,同时计算行数并度量执行计划中不同点上花费的时间。执行完成后,EXPLAIN ANALYZE 将输出计划和度量结果,而不是查询结果。这项新功能建立在常规的 EXPLAIN 基础之上,可以看作是 MySQL 8.0 之前添加的 EXPLAIN FORMAT = TREE 的扩展(8.0.17)。EXPLAIN 除了输出查询计划和估计成本之外,EXPLAIN ANALYZE 还会输出执行计划中各个迭代器的实际成本。

如何使用EXPLAIN ANALYZE

我们将使用 Sakila 样本数据库中的数据和一个查询举例说明,该查询列出了每个工作人员在 2005 年 8 月累积的总金额。查询非常简单:

mysql>SELECT first_name, last_name, SUM(amount) AS total
FROM staff INNER JOIN payment
ON staff.staff_id = payment.staff_id
AND
payment_date LIKE '2005-08%'
GROUP BY first_name, last_name;

+------------+-----------+----------+
| first_name | last_name | total |
+------------+-----------+----------+
| Mike | Hillyer | 11853.65 |
| Jon | Stephens | 12218.48 |
+------------+-----------+----------+
2 rows in set (0,02 sec)

只有两个人,Mike 和 Jon,我们在 2005 年 8 月获得了他们的总数。

EXPLAIN FORMAT = TREE 将向我们显示查询计划和成本估算:

mysql>EXPLAIN FORMAT=TREE
mysql>SELECT first_name, last_name, SUM(amount) AS total
FROM staff INNER JOIN payment
ON staff.staff_id = payment.staff_id
AND
payment_date LIKE '2005-08%'
GROUP BY first_name, last_name;

-> Table scan on 
-> Aggregate using temporary table
-> Nested loop inner join (cost=1757.30 rows=1787)
-> Table scan on staff (cost=3.20 rows=2)
-> Filter: (payment.payment_date like '2005-08%') (cost=117.43 rows=894)
-> Index lookup on payment using idx_fk_staff_id (staff_id=staff.staff_id) (cost=117.43 rows=8043)

但这并不能表明这些估计是否正确,或者查询计划实际上是在哪些操作上花费的时间。EXPLAIN ANALYZE 将执行以下操作:

mysql>EXPLAIN ANALYZE
mysql>SELECT first_name, last_name, SUM(amount) AS total
FROM staff INNER JOIN payment
ON staff.staff_id = payment.staff_id
AND
payment_date LIKE '2005-08%'
GROUP BY first_name, last_name;

-> Table scan on  (actual time=0.001..0.001 rows=2 loops=1)
-> Aggregate using temporary table (actual time=58.104..58.104 rows=2 loops=1)
-> Nested loop inner join (cost=1757.30 rows=1787) (actual time=0.816..46.135 rows=5687 loops=1)
-> Table scan on staff (cost=3.20 rows=2) (actual time=0.047..0.051 rows=2 loops=1)
-> Filter: (payment.payment_date like '2005-08%') (cost=117.43 rows=894) (actual time=0.464..22.767 rows=2844 loops=2)
-> Index lookup on payment using idx_fk_staff_id (staff_id=staff.staff_id) (cost=117.43 rows=8043) (actual time=0.450..19.988 rows=8024 loops=2)

这里有几个新的度量:

  • 获取第一行的实际时间(以毫秒为单位)
  • 获取所有行的实际时间(以毫秒为单位)
  • 实际读取的行数
  • 实际循环数

让我们看一个具体的示例,使用过滤条件的迭代器成本估算和实际度量,该迭代器过滤 2005 年 8 月的数据(上面 EXPLAIN ANALYZE 输出中的第 13 行)。

Filter: (payment.payment_date like '2005-08%')
(cost=117.43 rows=894)
(actual time=0.464..22.767 rows=2844 loops=2)

我们的过滤器的估计成本为 117.43,并且估计返回 894 行。这些估计是由查询优化器根据可用统计信息在执行查询之前进行的。该信息也会在 EXPLAIN FORMAT = TREE 输出中。

我们将从最后面的循环数开始。此过滤迭代器的循环数为 2。

这是什么意思?要了解此数字,我们必须查看查询计划中过滤迭代器上方的内容。

在第 11 行上,有一个嵌套循环联接,在第 12 行上,是在staff 表上进行表扫描。

这意味着我们正在执行嵌套循环连接,在其中扫描 staff 表,然后针对该表中的每一行,使用索引查找和过滤的付款日期来查找 payment 表中的相应条目。由于 staff 表中有两行(Mike 和 Jon),因此我们在第 14 行的索引查找上获得了两个循环迭代。

对于许多人来说,EXPLAIN ANALYZE 提供的最有趣的新信息是实际时间“ 0.464..22.767”,这意味着平均花费 0.464 毫秒读取第一行,而花费 22.767 毫秒读取所有行。平均时间?是的,由于存在循环,我们必须对该迭代器进行两次计时,并且报告的数字是所有循环迭代的平均值。这意味着过滤的实际执行时间是这些数字的两倍。

如果我们看一下在嵌套循环迭代器(第 11 行)中上一级接收所有行的时间,为 46.135 毫秒,这是运行一次过滤迭代器的时间的两倍多。这个时间反映了整个子树在执行过滤操作时的根部时间,即,使用索引查找迭代器读取行,然后评估付款日期为 2005 年 8 月的时间。

如果我们查看索引循环迭代器(第 14 行),我们看到相应的数字分别为 0.450 和 19.988ms。这意味着大部分时间都花在了使用索引查找来读取行上,并且与读取数据相比,实际的过滤成本相对低廉。实际读取的行数为 2844,而估计为 894 行。优化器错过了 3 倍的因素。同样,由于循环,估计值和实际值都是所有循环迭代的平均值。

如果我们查看 schema,发现 payment_date 列上没有索引或直方图,因此提供给优化器的统计信息是有限的。如果使用更好的统计信息可以得出更准确的估计值,我们可以再次查看索引查找迭代器。我们看到该索引提供了更加准确的统计信息:估计 8043 行与 8024 实际读取行。发生这种情况是因为索引附带了额外的统计信息,而这些数据对于非索引列是不存在的。

那么用户可以使用这些信息做什么?需要一定的练习,用户才可以分析查询并理解为什么它们表现不佳。但是,这里有一些帮助入门的简单提示:

  • 如果疑惑为何花费这么长时间,请查看时间。执行时间花在哪里?
  • 如果您想知道为什么优化器选择了该计划,请查看行计数器。如果估计的行数与实际的行数之间存在较大差异(即,几个数量级或更多),需要仔细看一下。优化器根据估算值选择计划,但是查看实际执行情况可能会告诉您,另一个计划会更好。

EXPLAIN ANALYZE 是 MySQL 查询分析工具里面的一个新工具:

  • 检查查询计划:EXPLAIN FORMAT = TREE
  • 分析查询执行:EXPLAIN ANALYZE
  • 了解计划选择:OPTIMIZER TRACE

新版本支持以tree方式展现,而且在关键步骤都有cost,可以非常方便,直观的看到sql哪一步有性能瓶颈,下面个一个示例

mysql> EXPLAIN FORMAT=tree select a.name,a.depno,b.name from t_test1 a inner join t_test2 b on a.depno=b.depno;

| -> Inner hash join (a.depno = b.depno) (cost=2.60 rows=6)

-> Table scan on a (cost=0.15 rows=6)

-> Hash

-> Index scan on b using name (cost=0.55 rows=3)

|

1 row in set (0.03 sec)

 

 

参考

技术分享 | MySQL EXPLAIN ANALYZE:https://segmentfault.com/a/1190000020803190

查询优化的百科全书 —— Explain 详解:https://www.yuque.com/uqfa83/fa17i6/akgde7、https://www.yuque.com/uqfa83/fa17i6/wtlts1

详解Mysql执行计划explain:https://blog.csdn.net/weixin_38004638/article/details/103300300

 

你可能感兴趣的:(数据库)