MySQL中执行计划explain命令示例详解
前言
explain命令是查看查询优化器如何决定执行查询的主要方法。
这个功能有局限性,并不总会说出真相,但它的输出是可以获取的最好信息,值得花时间去了解,因为可以学习到查询是如何执行的。
调用EXPLAIN
在select之前添加explain,mysql会在查询上设置一个标记,当执行查询计划时,这个标记会使其返回关于执行计划中每一步的信息,而不是执行它。
它会返回一行或多行信息,显示出执行计划中的每一部分和执行次序。
这是一个简单的explain效果:
在查询中每个表在输出只有一行,如果查询是两个表的联接,那么输出中将有两行。
别名表单算为一个表,因此,如果把一个表与自己联接,输出中也会有两行。
“表”的意义在这里相当广,可以是一个子查询,一个union结果等。
同时 explain有两个变种
EXPLAIN EXTENDED会告诉服务器“逆向编译” 执行计划为一个select语句。
可以通过紧接其后运行show warnings看到这个生成的语句,这个语句直接来自执行计划,而不是原SQL语句,到这点上已经变成一个数据结构。
大部分场景下,它都与原语句不相同,你可以检测查询偶花旗到底是如何转化语句的。
EXPLAIN EXTENDED在mysql 5.0 以上版本中可用,在5.1中增加了一个filtered列。
EXPLAIN PARTITIONS会显示查询将访问的分区,如果查询是基于分区表的话。
在mysql 5.1以上的版本中会存在。
EXPLAIN限制:
· explain根本不会告诉你触发器、存储过程或UDF会如何影响查询
· 不支持存储过程,尽管可以手动抽取查询并单独地对其进行explain操作
· 它并不会告诉你mysql在执行计划中所做的特定优化
· 它并不会显示关于查询的执行计划的所有信息
· 它并不区分具有相同名字的事物,例如,它对内存排列和临时文件都使用“filesort”,并且对于磁盘上和内存中的临时表都显示“Using temporary”
· 可能会产生误导,比如,它会对一个有着很小limit的查询显示全索引扫描(mysql 5.1的explain关于检查的行数会显示更精准的信息,但早期版本并不考虑limit)
重写非SELECT查询
mysql explain只能解释select查询,并不会对存储程序调用和insert、update、delete或其他语句做解释。然而,你可以重写某些非select查询以利用explain。为了达到这个目的,只需要将该语句转化成一个等价的访问所有相同列的select,任何体积的列都必须在select列表,关联子句,或者where子句中。
假如,你想重写下面的update语句使其可以利用explain
UPDATE sakila.actor INNER JOIN sakila.film_actor USING (actor_id) SET actor.last_update=film_actor.last_update;
下面的explain语句并不等价于上面的update,因为它并不要求服务器从任何一个表上获取last_update列
这个差别非常重要。例如,输出结果显示mysql将使用覆盖索引,但是,当检索并更新last_updated列时,就无法使用覆盖索引了,下面这种改写法就更接近原来的语句:
像这样重写查询并不非常科学,但对帮助理解查询是怎么做的已经足够好了。
(MySQL 5.6将允许解释非SELECT查询)
显示查询计划时,对于写查询并没有“等价”的读查询,理解这一点非常重要。一个SELECT查询只需要找到数据的一份副本并返回。而任何修改数据的查询必须在所有索引上查找并修改其所有副本,这常常比看起来等价的SELECT查询的消耗要高得多。
EXPLAIN中的列
将在下一部分展示explain结果中每一列的意义。
输出中的行以mysql实际执行的查询部分的顺序出现,而这个顺序不总是与其在原始SQL中的一致。
【id列】
这一列总是包含一个编号,识别select所属的行,如果在语句当中没有子查询或联合,那么只会有唯一的select,于是每一行在这个列中都将显示一个1,否则,内层的select语句一般会顺序编号,对应于其在原始语句中的位置。
mysql将select查询氛围简单和复杂类型,复杂类型可分为三大类:简单子查询,所谓的派生表(在from子句中的子查询),以及union查询。
下面是一个简单的子查询:
from子句中的子查询和联合给id列增加了更多的复杂性。
下面是一个from子句中的基本子查询:
如你所知,这个查询执行时有一个匿名的临时表,mysql内部通过别名(der)在外层查询中引用这个临时表,在更复杂的查询中可以看到ref列。
最后下面是一个union查询:
注意第三个额外的行,union的结果总是放在一个匿名临时表中,之后mysql将结果读取到临时表外,临时表并不在原SQL中出现,因此它的id列为null。
与之前的例子相比(演示子查询的那个from子句中),从这个查询产生的临时表在结果中出现在最后一行,而不是第一行。
到目前为止这些都非常直截了当,但这三类语句的混合则会使输出变得非常复杂,我们稍后就会看到。
【select_type列】
这一列显示了对应行是简单还是复杂的select(如果是后者,那么是三种复杂类型中的哪一种)。simple值意味着查询不包括子查询和union,如果查询有任何负责的子部分,则最外层部分标记为primary,其他部分标记如下:
SUBQUERY
包含在select列表中的子查询中的select(换句话说,不在from子句中)标记为SUBQUERY
DERIVED
DERIVED值用来表示包含在FROM子句的子查询中的select,mysql会递归执行并将结果放到一个临时表中。服务器内部称其“派生表”,因为该临时表是从子查询中派生来的。
UNION
在UNION中的第二个和随后的select被标记为unoin,第一个select被标记就好像它以部分外查询来执行。这就是之前的例子中在union中的第一个select显示为primary的原因。如果union被from子句中的子查询包含,那么它的第一个select会被标记为derived。
UNION RESULT
用来从union的匿名临时表检索结果的select被标记为UNION RESULT。
除了这些值,SUBQUERY和UNION还可以被标记为DEPENDENT何UNCACHEABLE。
DEPENDENT意味着select依赖于外层查询中发现的数据。
UNCACHEABLE意味着select中的某些特性阻止结果被缓存于一个Item_cache中。
(Item_cache未被文档记载,它与查询缓存不是一回事,尽管它可以被一些相同类型的构件否定,例如RAND()函数。)
【table列】
这一列显示了对应行正在访问哪个表,在通常情况下,它相当明了:它就是哪个表,或是该表的列明(如果SQL中定义了别名)。
可以在这一列中从上往下观察mysql的关联优化器为查询选择的关联顺序,例如,可以看到在下面的查询中mysql选择的关联顺序不同于语句中所指定的顺序:
mysql的执行计划总是左侧深度优先树,如果把这个计划放倒,就能按顺序读出叶子节点,它们直接对应于explain中的行,之前的查询计划看起来如下图所示:
派生表和联合
当from子句中有子查询或有union时,table列会变得复杂很多,这些场景下,确实没有一个“表”可以参考到,因为mysql创建的匿名临时表仅在查询执行过程中存在。
当在from子句中有子查询时,table列是
当有union时,union result的table列包含一个参与union的id列表。这总是“向后引用”,因为union result出现在union中所有参与行之后,如果在列表中有超过20个id,table列卡诺被截断以防止太长,此时不可能看到所有的值。幸运的是,仍然可以推测包括哪些行,因为你可以看到第一行的id,在这一行和union result之间出现的一切都会以某种方式被包含。
一个复杂select类型的例子
下面是一个无意义的查询,我们这里把它用作某种复杂select类型的紧凑示例
limit子句只是为了方便起见,以防你打算不以explain方式执行来看结果。
以下是explain的部分结果:
我们特意让每个查询部分访问不同的表,以便可以弄清楚问题所在,但仍然难以解决,从上面开始看起:
第1行向前引用了der_1,这个查询被标记为
第2行,它的id是3,因为它是查询中第3个select的一部分,归为derived类型是因为它嵌套在from子句中的子查询内部,在原sql中为第4行。
第3行的id为2,在原sql中为第3行,注意,它在具有更高id的行的后面,暗示后面再执行,这是合理的。它被归为DEPENDENT SUBQUERY,意味着其结果依赖于外层查询(亦即某个相关子查询)。本例中的外查询从第2行开始,从der_1中检索数据的select。
第4行被归为union,意味着它是union中的第2个或之后的select,它的表为
第5行是在原sql中的第8行的der_2子查询,explain称其为
第6行是
……因为它比5大,而5是第7行的id。为什么重要?因为它显示了
最后一行union result,它代表从union的临时表中读取行的阶段。你可以从这行开始反过来向后,如果你愿意的话,它返回id是1和4的行结果,它们分别引用了
如你所见,这些复杂的select类型的组合会使explain的输出相当难懂,理解规则会使其简单些,但仍然需要多时间。
阅读explain的输出经常需要在列表中跳来跳去,例如,再查看第1行输出,仅仅盯着看,是无法知道它是union的一部分的,只有看到最后1行你才会明白过来。
【type列】
mysql用户手册上说这一列显示了“关联类型”,但我们认为更准确的说法是访问类型——换言之就是mysql决定如何查找表中的行。下面是最重要的访问方法,依次从最差到最优:
ALL:
这就是所谓的全表扫描,意味着mysql必须扫描整张表,从头到尾,去找到需要的行。(有个例外,例如在查询里使用了limit,或者在extra列中显示“Using distinct/not exists”。
index:
这个跟全表扫描一样,只是mysql扫描表时按索引次序而不是行,它的主要优点是避免了排序;最大缺点是要承担按索引次序读取整个表的开销。这通常意味着若是按随机次序访问行,开销将非常大。
如果在extra列中看到“Using index”,说明mysql正在使用索引覆盖,它只扫描索引的数据,而不是按索引次序的每一行,它比按索引次序全表扫描的开销要少很多。
range:
范围扫描就是一个有限制的索引扫描,它开始与索引里的某一点,返回匹配这个值域的行,这比全索引扫描要好一点,因为它用不着遍历全部索引,显而易见的扫描是带有between或在where子句里带有>的查询。
当mysql使用索引去查找一系列值时,例如in()和or列表,也会显示为范围扫描,然而,这两者其实是相当不同的访问类型,在性能上有重要的差异。
此类扫描的开销跟索引类型的相当。
ref:
这是一种索引访问(有时也叫做索引查找),它返回所有匹配某个单个值的行,然而,它可能会找到多个符合条件的行,因此,它是查找和扫描的混合体,此类索引访问只有当使用非唯一性索引或者唯一性索引的非唯一性前缀时才会发生。把它叫做ref是因为索引要跟某个参考值相比较。这个参考值或者是一个常数,或者是来自多表查询前一个表里的结果值。
ref_or_null是ref之上的一个变体,它意味着mysql必须在初次查找的结果里进行第二次查找以找出null条目。
eq_ref:
使用这种索引查找,mysql知道最多只返回一条符合条件的记录,这种访问方法可以在mysql使用主键或者唯一性索引查找时看到,它会将它们与某个参考值做比较。mysql对于这类访问类型的优化做得非常好,因为它知道无需估计匹配行的范围或在找到匹配行后再继续查找。
const,system:
当mysql能对查询的某部分进行优化并将其转换成一个常量时,他就会使用这些访问类型,举例来说,如果你通过将某一行的主键放入where子句里的方式来选取此行的主键,mysql就能把这个查询转换为一个常量,然后就可以高效地将表从联接执行中移除。
null:
这种访问方式意味着mysql能在优化阶段分解查询语句,在执行阶段甚至用不着再访问表或者索引。例如,从一个索引列里选取最小值可以通过单独查找索引来完成,不需要在执行时访问表。
【possible_keys列】
这一列显示了查询可以使用哪些索引,这是基于查询访问的列和使用的比较操作符来判断的。这个列表是在优化过程的早期创建的,因此有些罗列出来的索引可能对于后续优化过程是没用的。
【key列】
这一列显示了mysql决定采用哪个索引来优化对该表的访问。如果该索引没有出现在possible_keys列中,那么mysql选用它是处于另外的原因——例如,它可能选择了一个覆盖索引,哪怕没有where子句。
换句话说,possible_keys揭示了哪一个索引能有助于高效地进行查找,而key显示的是优化采用哪一个索引可以最小化查询成本。下面是一个例子:
【key_len列】
该列显示了mysql在索引里使用的字节数,如果mysql正在使用的只是索引里的某些列,那么就可以用这个值来算出具体是哪些列,要记住,mysql 5.5及之前的版本只能使用索引的最左前缀,举例来说,film_actor的主键是两个smallint列,并且每个smallint列是两字节,那么索引中的每项是4字节,以下就是一个查询的示例:
基于结果中的key_len列,可以推断出查询使用唯一的首列——actor_id列,来执行索引查找,当我们计算列的使用情况时,务必把字符列中的字符集页考虑进去。
查看执行计划:
这个查询中平均长度为13字节,即为a列和b列的总长度,a列是3个字符,utf8下每一个最多为3字节,而b列是一个4字节整型。
mysql并不总是显示一个索引真正使用了多少,例如,如果对一个前缀模式匹配执行like查询,它会显示列的完全宽度正在被使用。
key_len列显示了在索引字段中可能的最大长度,而不是表中数据使用的实际字节数,在前面例子中mysql总是显示13字节,即使a列恰巧只包含一个字符长度。换言之,key_len通过查找表的定义而被计算出,而不是表中的数据。
【ref列】
这一列显示了之前的表在key列记录的索引中查找值所用的列或常量,下面是一个展示关联条件和别名组合的例子,注意,ref列反映了在查询文本中film表是如何以f为别名的:
【rows列】
这一列是mysql估计为了找到所需的行而要读取的行数。这个数字是内嵌循环关联计划里的 循环数目,也就是说它不是mysql认为它最终要从表里读取出来的行数,而是mysql为了找到符合查询的每一点上标准的那些行而必须读取的行的平均数。(这个标准包括sql里给定的条件,以及来自联接次序上前一个表的当前列。)
根据表的统计信息和索引的选用情况,这个估算可能很不精确,在mysql5.0及更早的版本里,它也反映不出limit子句,举例来说,下面这个查询不会真的检查1057行。
通过把所有rows列的值相乘,可以粗略地估算出整个查询会检查的行数,例如,以下这个查询大约会检查2600行。
要记住这个数字是mysql认为它要检查的行数,而不是结果集里的行数,同时也要认识到有很多优化手段,例如关联缓冲区和缓存,无法影响到行数的显示,mysql可能不必真的读所有它估计到的行,它也不知道任何关于操作系统或硬件缓存的信息。
【Extra列】
这一列包含的是不适合在其他列显示的额外信息。mysql用户手册里记录了大多数可以在这里出现的值。
常见的最重要的值如下。
“Using index”
此值表示mysql将使用覆盖索引,以避免访问表。不要把覆盖索引和index访问类型弄混了。
“Using where”
这意味着mysql服务器将在存储引擎检索行后再进行过滤,许多where条件里涉及索引中的列,当(并且如果)它读取索引时,就能被存储引擎检验,因此不是所有带where子句的查询都会显示“Using where”。有时“Using where”的出现就是一个暗示:查询可受益于不同的索引。
“Using temporary”
这意味着mysql在对查询结果排序时会使用一个临时表。
“Using filesort”
这意味着mysql会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。mysql有两种文件排序算法,这两种排序方式都可以在内存或者磁盘上完成,explain不会告诉你mysql将使用哪一种文件排序,也不会告诉你排序会在内存里还是磁盘上完成。
“Range checked for each record(index map: N)”
这个意味着没有好用的索引,新的索引将在联接的每一行上重新估算,N是显示在possible_keys列中索引的位图,并且是冗余的。
树形格式的输出
mysql用户往往更希望把explain的输出格式化成一棵树,更加精确地展示执行计划。实际上,explain查看执行计划的方式确实有点笨拙,树状结构也不适合表格化的输出,当extra列里有大量的值时,缺点更明显,使用union也是这样,union跟mysql能做的其他类型的联接不太一样,它不太适合explain。
如果对explain的规则和特性有充分了解,使用树形结构的执行计划也是可行的。但是这有点枯燥,最好还是留给自动化的工具处理,Percona Toolkit包含了pt-visual-explain,它就是这样一个工具。
MySQL 5.6中的改进
mysql5.6中将包含一个对explain的重要改进:能对类似update、insert等的查询进行解释,尽管可以将dml语句转化为等价的“select”查询并explain,但结果并不会完全反映语句是如何执行的,因而这仍然非常有帮助。在开发使用类似Percona Toolkit中的pt-upgrade时曾尝试使用过那个技术,我们不止一次发现,在将查询转化为select时,优化器并不能按我们预期的代码路径执行。因而explain一个查询而不需要转化为select,对我们理解执行过程中到底发生什么,是非常有帮助的。
mysql5.6还将包括对查询优化和执行引擎的一系列修改,允许匿名的临时表尽可能晚地被具体化,而不总是在优化和执行使用到此临时表的部分查询时创建并填充它们,这将允许mysql可以直接解释带子查询的查询语句,而不需要先实际地执行子查询。
最后,mysql5.6将通过在服务器中增加优化跟踪功能的方式改进优化器的相关部分,浙江允许用户查看优化器坐出的抉择,以及输入(例如,索引的基数)和抉择的原因。这非常有帮助,不仅仅对理解服务器选择的执行计划如此,对为什么选择这个计划也如此。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对潘少俊衡的支持。
版权声明
本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。
本文地址:/shujuku/MySQL/104875.html