【mysql】MySQL eplain 彻底解读

EXPLAIN做为MySQL的性能分析神器,读懂其结果是颇有必要的,然而我在各类搜索引擎上居然找不到特别完整的解读。都是只有重点,没有细节(例如type的取值不全、Extra缺少完整的介绍等)。html

因此,我肝了将近一个星期,整理了一下。这应该是全网最全面、最细致的EXPLAIN解读文章了,下面是全文。java

文章比较长,建议收藏。mysql

TIPS

本文基于MySQL 8.0编写,理论支持MySQL 5.0及更高版本。面试

EXPLAIN使用

explain可用来分析SQL的执行计划。格式以下:算法

{EXPLAIN | DESCRIBE | DESC}
    tbl_name [col_name | wild]

{EXPLAIN | DESCRIBE | DESC}
    [explain_type]
    {explainable_stmt | FOR CONNECTION connection_id}

{EXPLAIN | DESCRIBE | DESC} ANALYZE select_statement    

explain_type: {
    FORMAT = format_name
}

format_name: {
    TRADITIONAL
  | JSON
  | TREE
}

explainable_stmt: {
    SELECT statement
  | TABLE statement
  | DELETE statement
  | INSERT statement
  | REPLACE statement
  | UPDATE statement
}

示例:sql

EXPLAIN format = TRADITIONAL json SELECT tt.TicketNumber, tt.TimeIn,
               tt.ProjectReference, tt.EstimatedShipDate,
               tt.ActualShipDate, tt.ClientID,
               tt.ServiceCodes, tt.RepetitiveID,
               tt.CurrentProcess, tt.CurrentDPPerson,
               tt.RecordVolume, tt.DPPrinted, et.COUNTRY,
               et_1.COUNTRY, do.CUSTNAME
        FROM tt, et, et AS et_1, do
        WHERE tt.SubmitTime IS NULL
          AND tt.ActualPC = et.EMPLOYID
          AND tt.AssignedPC = et_1.EMPLOYID
          AND tt.ClientID = do.CUSTNMBR;

结果输出展现:json

id select_id 该语句的惟一标识
select_type 查询类型
table table_name 表名
partitions partitions 匹配的分区
type access_type 联接类型
possible_keys possible_keys 可能的索引选择
key key 实际选择的索引
key_len key_length 索引的长度
ref ref 索引的哪一列被引用了
rows rows 估计要扫描的行
filtered filtered 表示符合查询条件的数据百分比
Extra 没有 附加信息

结果解读

id

该语句的惟一标识。若是explain的结果包括多个id值,则数字越大越先执行;而对于相同id的行,则表示从上往下依次执行。缓存

select_type

查询类型,有以下几种取值:服务器

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

table

表示当前这一行正在访问哪张表,若是SQL定义了别名,则展现表的别名并发

partitions

当前查询匹配记录的分区。对于未分区的表,返回null

type

链接类型,有以下几种取值,性能从好到坏排序 以下:

1 system:该表只有一行(至关于系统表),system是const类型的特例

2 const:针对主键或惟一索引的等值查询扫描, 最多只返回一行数据. const 查询速度很是快, 由于它仅仅读取一次便可

3 eq_ref:当使用了索引的所有组成部分,而且索引是PRIMARY KEY或UNIQUE NOT NULL 才会使用该类型,性能仅次于system及const。

-- 多表关联查询,单行匹配
SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column=other_table.column;

-- 多表关联查询,联合索引,多行匹配
SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column_part1=other_table.column
  AND ref_table.key_column_part2=1;

4 ref:当知足索引的最左前缀规则,或者索引不是主键也不是惟一索引时才会发生。若是使用的索引只会匹配到少许的行,性能也是不错的。

-- 根据索引(非主键,非惟一索引),匹配到多行
SELECT * FROM ref_table WHERE key_column=expr;

-- 多表关联查询,单个索引,多行匹配
SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column=other_table.column;

-- 多表关联查询,联合索引,多行匹配
SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column_part1=other_table.column
  AND ref_table.key_column_part2=1;

TIPS

最左前缀原则,指的是索引按照最左优先的方式匹配索引。好比建立了一个组合索引(column1, column2, column3),那么,若是查询条件是:

  • WHERE column1 = 一、WHERE column1= 1 AND column2 = 二、WHERE column1= 1 AND column2 = 2 AND column3 = 3 均可以使用该索引;
  • WHERE column1 = 二、WHERE column1 = 1 AND column3 = 3就没法匹配该索引。

5 fulltext:全文索引

6 ref_or_null:该类型相似于ref,可是MySQL会额外搜索哪些行包含了NULL。这种类型常见于解析子查询

SELECT * FROM ref_table
  WHERE key_column=expr OR key_column IS NULL;

7 index_merge:此类型表示使用了索引合并优化,表示一个查询里面用到了多个索引

8 unique_subquery:该类型和eq_ref相似,可是使用了IN查询,且子查询是主键或者惟一索引。例如:

value IN (SELECT primary_key FROM single_table WHERE some_expr)

9 index_subquery:和unique_subquery相似,只是子查询使用的是非惟一索引

value IN (SELECT key_column FROM single_table WHERE some_expr)

10 range:范围扫描,表示检索了指定范围的行,主要用于有限制的索引扫描。比较常见的范围扫描是带有BETWEEN子句或WHERE子句里有>、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE、IN()等操做符。

SELECT * FROM tbl_name
  WHERE key_column BETWEEN 10 and 20;

SELECT * FROM tbl_name
  WHERE key_column IN (10,20,30);

11 index:全索引扫描,和ALL相似,只不过index是全盘扫描了索引的数据。当查询仅使用索引中的一部分列时,可以使用此类型。有两种场景会触发:

  • 若是索引是查询的覆盖索引,而且索引查询的数据就能够知足查询中所需的全部数据,则只扫描索引树。此时,explain的Extra 列的结果是Using index。index一般比ALL快,由于索引的大小一般小于表数据。
  • 按索引的顺序来查找数据行,执行了全表扫描。此时,explain的Extra列的结果不会出现Uses index。
  • ALL:全表扫描,性能最差。

possible_keys

展现当前查询可使用哪些索引,这一列的数据是在优化过程的早期建立的,所以有些索引可能对于后续优化过程是没用的。

key

表示MySQL实际选择的索引

key_len

索引使用的字节数。因为存储格式,当字段容许为NULL时,key_len比不容许为空时大1字节。

key_len计算公式: https://www.cnblogs.com/gomysql/p/4004244.html

ref

表示将哪一个字段或常量和key列所使用的字段进行比较。

若是ref是一个函数,则使用的值是函数的结果。要想查看是哪一个函数,可在EXPLAIN语句以后紧跟一个SHOW WARNING语句。

rows

MySQL估算会扫描的行数,数值越小越好。

filtered

表示符合查询条件的数据百分比,最大100。用rows × filtered可得到和下一张表链接的行数。例如rows = 1000,filtered = 50%,则和下一张表链接的行数是500。

TIPS

在MySQL 5.7以前,想要显示此字段需使用explain extended命令;

MySQL.5.7及更高版本,explain默认就会展现filtered

Extra

展现有关本次查询的附加信息,取值以下:

1 Child of 'table' pushed join@1

此值只会在NDB Cluster下出现。

2 const row not found

例如查询语句SELECT ... FROM tbl_name,而表是空的

3 Deleting all rows

对于DELETE语句,某些引擎(例如MyISAM)支持以一种简单而快速的方式删除全部的数据,若是使用了这种优化,则显示此值

4 Distinct

查找distinct值,当找到第一个匹配的行后,将中止为当前行组合搜索更多行

5 FirstMatch(tbl_name)

当前使用了半链接FirstMatch策略,详见 https://mariadb.com/kb/en/firstmatch-strategy/ ,翻译 https://www.cnblogs.com/abclife/p/10895624.html

6 Full scan on NULL key

子查询中的一种优化方式,在没法经过索引访问null值的时候使用

7 Impossible HAVING

HAVING子句始终为false,不会命中任何行

8 Impossible WHERE

WHERE子句始终为false,不会命中任何行

9 Impossible WHERE noticed after reading const tables

MySQL已经读取了全部const(或system)表,并发现WHERE子句始终为false

10 LooseScan(m..n)

当前使用了半链接LooseScan策略,详见 https://mariadb.com/kb/en/loosescan-strategy/ ,翻译 http://www.javacoder.cn/?p=39

11 No matching min/max row

没有任何能知足例如 SELECT MIN(...) FROM ... WHERE condition 中的condition的行

12 no matching row in const table

对于关联查询,存在一个空表,或者没有行可以知足惟一索引条件

13 No matching rows after partition pruning

对于DELETE或UPDATE语句,优化器在partition pruning(分区修剪)以后,找不到要delete或update的内容

14 No tables used

当此查询没有FROM子句或拥有FROM DUAL子句时出现。例如:explain select 1

15 Not exists

MySQL能对LEFT JOIN优化,在找到符合LEFT JOIN的行后,不会为上一行组合中检查此表中的更多行。例如:

SELECT * FROM t1 LEFT JOIN t2 ON t1.id=t2.id
  WHERE t2.id IS NULL;

假设t2.id定义成了NOT NULL ,此时,MySQL会扫描t1,并使用t1.id的值查找t2中的行。 若是MySQL在t2中找到一个匹配的行,它会知道t2.id永远不会为NULL,而且不会扫描t2中具备相同id值的其他行。也就是说,对于t1中的每一行,MySQL只须要在t2中只执行一次查找,而不考虑在t2中实际匹配的行数。

在MySQL 8.0.17及更高版本中,若是出现此提示,还可表示形如 NOT IN (subquery) 或 NOT EXISTS (subquery) 的WHERE条件已经在内部转换为反链接。这将删除子查询并将其表放入最顶层的查询计划中,从而改进查询的开销。经过合并半链接和反联接,优化器能够更加自由地对执行计划中的表从新排序,在某些状况下,可以让查询提速。你能够经过在EXPLAIN语句后紧跟一个SHOW WARNING语句,并分析结果中的Message列,从而查看什么时候对该查询执行了反联接转换。

Note

两表关联只返回主表的数据,而且只返回主表与子表没关联上的数据,这种链接就叫反链接

16 Plan isn't ready yet

使用了EXPLAIN FOR CONNECTION,当优化器还没有完成为在指定链接中为执行的语句建立执行计划时, 就会出现此值。

17 Range checked for each record (index map: N)

MySQL没有找到合适的索引去使用,可是去检查是否可使用range或index_merge来检索行时,会出现此提示。index map N索引的编号从1开始,按照与表的SHOW INDEX所示相同的顺序。 索引映射值N是指示哪些索引是候选的位掩码值。 例如0x19(二进制11001)的值意味着将考虑索引一、4和5。

示例:下面例子中,name是varchar类型,可是条件给出整数型,涉及到隐式转换。 图中t2也没有用到索引,是由于查询以前我将t2中name字段排序规则改成utf8_bin致使的连接字段排序规则不匹配。

explain select a.* from t1 a left  join t2 b
on t1.name = t2.name
where t2.name = 2;

结果:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t2 NULL ALL idx_name NULL NULL NULL 9 11.11 Using where
1 SIMPLE t1 NULL ALL idx_name NULL NULL NULL 5 11.11 Range checked for each record (index map: 0x8)

18 Recursive

出现了递归查询。详见 “WITH (Common Table Expressions)”

19 Rematerialize

用得不多,使用相似以下SQL时,会展现Rematerialize

SELECT
  ...
FROM
  t,
  LATERAL (derived table that refers to t) AS dt
...

20 Scanned N databases

表示在处理INFORMATION_SCHEMA表的查询时,扫描了几个目录,N的取值能够是0,1或者all。详见 “Optimizing INFORMATION_SCHEMA Queries”

21 Select tables optimized away

优化器肯定:①最多返回1行;②要产生该行的数据,要读取一组肯定的行,时会出现此提示。通常在用某些聚合函数访问存在索引的某个字段时,优化器会经过索引直接一次定位到所须要的数据行完成整个查询时展现,例以下面这条SQL。

explain
select min(id)
from t1;

22 Skip_open_table, Open_frm_only, Open_full_table

这些值表示适用于INFORMATION_SCHEMA表查询的文件打开优化;

23 Skip_open_table:无需打开表文件,信息已经经过扫描数据字典得到

24 Open_frm_only:仅须要读取数据字典以获取表信息

25 Open_full_table:未优化的信息查找。表信息必须从数据字典以及表文件中读取

26 Start temporary, End temporary

表示临时表使用Duplicate Weedout策略,详见 https://mariadb.com/kb/en/duplicateweedout-strategy/ ,翻译 https://www.cnblogs.com/abclife/p/10895531.html

27 unique row not found

对于形如 SELECT ... FROM tbl_name 的查询,但没有行可以知足惟一索引或主键查询的条件

28 Using filesort

当Query 中包含 ORDER BY 操做,并且没法利用索引完成排序操做的时候,MySQL Query Optimizer 不得不选择相应的排序算法来实现。数据较少时从内存排序,不然从磁盘排序。Explain不会显示的告诉客户端用哪一种排序。官方解释:“MySQL须要额外的一次传递,以找出如何按排序顺序检索行。经过根据联接类型浏览全部行并为全部匹配WHERE子句的行保存排序关键字和行的指针来完成排序。而后关键字被排序,并按排序顺序检索行”

29 Using index

仅使用索引树中的信息从表中检索列信息,而没必要进行其余查找以读取实际行。当查询仅使用属于单个索引的列时,可使用此策略。例如:

explain SELECT id FROM t

30 Using index condition

表示先按条件过滤索引,过滤完索引后找到全部符合索引条件的数据行,随后用 WHERE 子句中的其余条件去过滤这些数据行。经过这种方式,除非有必要,不然索引信息将能够延迟“下推”读取整个行的数据。详见 “Index Condition Pushdown Optimization” 。例如:

TIPS

  • MySQL分红了Server层和引擎层,下推指的是将请求交给引擎层处理。
  • 理解这个功能,可建立因此INDEX (zipcode, lastname, firstname),并分别用以下指令,

    SET optimizer_switch = 'index_condition_pushdown=off'; 
    SET optimizer_switch = 'index_condition_pushdown=on';

    开或者关闭索引条件下推,并对比:

    explain SELECT * FROM people
      WHERE zipcode='95054'
      AND lastname LIKE '%etrunia%'
      AND address LIKE '%Main Street%';

    的执行结果。

  • index condition pushdown从MySQL 5.6开始支持,是MySQL针对特定场景的优化机制,感兴趣的能够看下 https://blog.51cto.com/lee90/2060449

31 Using index for group-by

数据访问和 Using index 同样,所需数据只需要读取索引,当Query 中使用GROUP BY或DISTINCT 子句时,若是分组字段也在索引中,Extra中的信息就会是 Using index for group-by。详见 “GROUP BY Optimization”

-- name字段有索引
explain SELECT name FROM t1 group by name

32 Using index for skip scan

表示使用了Skip Scan。详见 Skip Scan Range Access Method

33 Using join buffer (Block Nested Loop), Using join buffer (Batched Key Access)

使用Block Nested Loop或Batched Key Access算法提升join的性能。详见 https://www.cnblogs.com/chenpingzhao/p/6720531.html

34 Using MRR

使用了Multi-Range Read优化策略。详见 “Multi-Range Read Optimization”

35 Using sort_union(...), Using union(...), Using intersect(...)

这些指示索引扫描如何合并为index_merge链接类型。详见 “Index Merge Optimization”

36 Using temporary

为了解决该查询,MySQL须要建立一个临时表来保存结果。若是查询包含不一样列的GROUP BY和 ORDER BY子句,一般会发生这种状况。

-- name无索引
explain SELECT name FROM t1 group by name

37 Using where

若是咱们不是读取表的全部数据,或者不是仅仅经过索引就能够获取全部须要的数据,则会出现using where信息

explain SELECT * FROM t1 where id > 5

38 Using where with pushed condition

仅用于NDB

39 Zero limit

该查询有一个limit 0子句,不能选择任何行

explain SELECT name FROM resource_template limit 0

扩展的EXPLAIN

EXPLAIN可产生额外的扩展信息,可经过在EXPLAIN语句后紧跟一条SHOW WARNING语句查看扩展信息。

TIPS

  • 在MySQL 8.0.12及更高版本,扩展信息可用于SELECT、DELETE、INSERT、REPLACE、UPDATE语句;在MySQL 8.0.12以前,扩展信息仅适用于SELECT语句;
  • 在MySQL 5.6及更低版本,需使用EXPLAIN EXTENDED xxx语句;而从MySQL 5.7开始,无需添加EXTENDED关键词。

使用示例:

mysql> EXPLAIN
       SELECT t1.a, t1.a IN (SELECT t2.a FROM t2) FROM t1\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t1
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 4
     filtered: 100.00
        Extra: Using index
*************************** 2. row ***************************
           id: 2
  select_type: SUBQUERY
        table: t2
         type: index
possible_keys: a
          key: a
      key_len: 5
          ref: NULL
         rows: 3
     filtered: 100.00
        Extra: Using index
2 rows in set, 1 warning (0.00 sec)

mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `test`.`t1`.`a` AS `a`,
         <in_optimizer>(`test`.`t1`.`a`,`test`.`t1`.`a` in
         ( <materialize> (/* select#2 */ select `test`.`t2`.`a`
         from `test`.`t2` where 1 having 1 ),
         <primary_index_lookup>(`test`.`t1`.`a` in
         <temporary table> on <auto_key>
         where ((`test`.`t1`.`a` = `materialized-subquery`.`a`))))) AS `t1.a
         IN (SELECT t2.a FROM t2)` from `test`.`t1`
1 row in set (0.00 sec)

因为SHOW WARNING的结果并不必定是一个有效SQL,也不必定可以执行(由于里面包含了不少特殊标记)。特殊标记取值以下:

1 <auto_key>

自动生成的临时表key

2 <cache>(expr)

表达式(例如标量子查询)执行了一次,而且将值保存在了内存中以备之后使用。对于包括多个值的结果,可能会建立临时表,你将会看到 <temporary table> 的字样

3 <exists>(query fragment)

子查询被转换为 EXISTS

4 <in_optimizer>(query fragment)

这是一个内部优化器对象,对用户没有任何意义

5 <index_lookup>(query fragment)

使用索引查找来处理查询片断,从而找到合格的行

6 <if>(condition, expr1, expr2)

若是条件是true,则取expr1,不然取expr2

7 <is_not_null_test>(expr)

验证表达式不为NULL的测试

8 <materialize>(query fragment)

使用子查询实现

9 materialized-subquery.col_name

在内部物化临时表中对col_name的引用,以保存子查询的结果

10 <primary_index_lookup>(query fragment)

使用主键来处理查询片断,从而找到合格的行

11 <ref_null_helper>(expr)

这是一个内部优化器对象,对用户没有任何意义

12 /* select#N */ select_stmt

SELECT与非扩展的EXPLAIN输出中id=N的那行关联

13 outer_tables semi join (inner_tables)

半链接操做。inner_tables展现未拉出的表。详见 “Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations”

14 <temporary table>

表示建立了内部临时表而缓存中间结果

当某些表是const或system类型时,这些表中的列所涉及的表达式将由优化器尽早评估,而且不属于所显示语句的一部分。可是,当使用FORMAT=JSON时,某些const表的访问将显示为ref。

估计查询性能

多数状况下,你能够经过计算磁盘的搜索次数来估算查询性能。对于比较小的表,一般能够在一次磁盘搜索中找到行(由于索引可能已经被缓存了),而对于更大的表,你可使用B-tree索引进行估算:你须要进行多少次查找才能找到行:log(row_count) / log(index_block_length / 3 * 2 / (index_length + data_pointer_length)) + 1

在MySQL中,index_block_length一般是1024字节,数据指针通常是4字节。比方说,有一个500,000的表,key是3字节,那么根据计算公式 log(500,000)/log(1024/3*2/(3+4)) + 1 = 4 次搜索。

该索引将须要500,000 * 7 * 3/2 = 5.2MB的存储空间(假设典型的索引缓存的填充率是2/3),所以你能够在内存中存放更多索引,可能只要一到两个调用就能够找到想要的行了。

可是,对于写操做,你须要四个搜索请求来查找在何处放置新的索引值,而后一般须要2次搜索来更新索引并写入行。

前面的讨论并不意味着你的应用性能会由于log N而缓慢降低。只要内容被OS或MySQL服务器缓存,随着表的变大,只会稍微变慢。在数据量变得太大而没法缓存后,将会变慢不少,直到你的应用程序受到磁盘搜索约束(按照log N增加)。为了不这种状况,能够根据数据的增加而增长key的。对于MyISAM表,key的缓存大小由名为key_buffer_size的系统变量控制,详见 Section 5.1.1, “Configuring the Server”

参考文档

相关文章
相关标签/搜索