Mysql调优-explain的使用

我的梦境 提交于 2020-01-27 18:55:08

explain简介

explain和SQL语句一起使用的时候,MySQL将显示来自优化器的相关语句执行计划的信息,包括表的读取顺序、数据读取操作的操作类型、那些索引可以被使用、那些索引会被实际使用、表之间的引用、每张表大概需要读取多少行记录等等。通过这些我们能分析SQL语句的结构和性能瓶颈,从而优化我们的SQL语句。

example:

explain输出列解释

字段 字段说明 字段值描述
id select查询序列号,包含一组数字,表示查询中执行select子句或者操作表的顺序 id相同 执行顺序由上至下
id不相同 如果是子查询,id的序号会递增,id的值越大被执行的优先级越高
id相同和不相同都存在 如果id相同可以认为是一组,同一组id执行顺序由上至下,不同组之间,id值越大被执行的优先级越高
select_type 查询的类型,主要用来区别普通查询,联合查询,子查询等复杂查询 SIMPLE 简单的select查询,查询中不包含子查询或者UNION
PRIMARY 查询中若包含任何复杂的子部分,最外层的查询则被标记为PRIMARY
SUBQUERY 在select或者where列表中包含了子查询
DERIVED 在from列表中包含的子查询会被标记为DERICED(衍生),Mysql会递归地执行这些子查询,然后把结果放到临时表
UNION 若第二个select语句出现在UNION之后,则被标记为UNION。若UNION包含在from子句的子查询中,外层select则被标记为DERIVED
UNION RESULT 从union表获取结果的select
type 访问类型排序,显示查询使用了何种类型,从最好到最差依次是:system>const>eq_ref>ref>range>index>ALL system 表只有一行记录,这是const类型的特例,这个平时很少出现
const 表最多有一个匹配行,它将在查询开始时被读取。因为仅有一行记录匹配,所以这行的列值可被优化器剩余部分认为是常数。const表很快,因为它们只读取一次!
eq_ref 驱动表和关联表中的每行进行组合并且仅有一行记录。这是除了system 和 const 类型之外, 这是最好的联接类型。当连接使用索引的所有部分时, 索引是主键或唯一非 NULL 索引时, 将使用该值
ref 非唯一性索引扫描,返回符合某个索引值的所有记录,可能会有多条记录匹配
range 使用一个索引来检索给定范围的行,这种范围索引扫描比全表扫描效率要高
index 使用覆盖索引
all 全表扫描(full table scan)
possiable_keys 显示可能应用在这张表中的索引,一个或者多个,查询的涉及的字段若存在索引,则该索引被列出,但是不一定被查询实际用到
key 实际使用的索引,如果为NULL,则没有使用索引;若查询中使用了覆盖索引,则该索引只会出现在key列表中
key_len 表示索引中使用的字节数,可通过该列计算查询中所使用索引的长度,在损失精度的情况下,索引长度越短越好;key_len显示的值为索引字段最大的可能长度,并非实际使用的长度,即key_len是根据表的定义计算而得,并非通过表内检索出的
ref 哪些列或者常量被用做索引列上的值
rows 根据表的统计信息和索引的使用情况,大致估算查询结果所需要读取记录的行数
extra 包含其explain字段不适合显示但又十分重要的额外信息

explain输出列-id

  • id相同:执行顺序由上至下

    Id列的结果值全部是1,表示由上到下,先查询game_company表,再查询game表,最后查询game_type表

  • id不同:如果是子查询,id的序号会递增,id的值越大被执行的优先级越高

    id的结果值分别是123,最里面的子查询country值最大优先级最高,最先被执行,接着是子查询game_company,最后才是主查询game。其中select_type的值PRIMARY、SUBQUERY分别表示主查询和子查询。

  • id同时存在相同和不相同:id相同为同一组,组内执行顺序由上至下,组之间id值越大被执行的优先级越高

    上面例子中id=1的表<derived2>,game为一组,id=2的表game_type为一组,所以回优先查询表game_type,接着到id=1的组内,由上至下先查询表<derived>再查询表game。

    tips:

    • <derived2>是派生表2的意思,后面select_type会讲到。
    • form子查询语句中加LIMIT 2的原因是在MYSQL 5.7后,引入了derived_merge,一种查询优化技术,作用就是把派生表合并到外部的查询中,提高数据检索的效率。举例来说:“select * from (select * from a) aa where aa.id =1;”,在derived_merge下会被优化成"select * from a where a.id =1;",但是当派生子查询存在以下操作时该特性无法生效:UNION 、GROUP BY、DISTINCT、LIMIT/OFFSET以及聚合操作。由于上面做实验的例子比较简单在derived_merge打开的情况下from子查询会合并到外层查询,无法演示效果,因此在子查询语句中加了limit,当然也可以通过设置属性"optimizer_switch='derived_merge=off"来关闭derived_merge。

explain输出列-select_type

  • SIMPLE:简单的select查询,查询中不包含子查询或者UNION

  • PRIMARY:查询中若包含任何复杂的子部分,最外层的查询则被标记为PRIMARY

    上图表示,select * game 是主查询。

  • SUBQUERY:在select或者where列表中包含了子查询

    where列表中的子查询select id from game_type where type_name = “RTS”。

    select列表中的子查询select game_type.type_name from game_type where game_type.id = 1

  • DERIVED:在from列表中包含的子查询会被标记为DERICED(衍生),Mysql会递归地执行这些子查询,然后把结果放到临时表

    上图第三行结果select_type=DERIVED,表示from列表中的子查询select * from game_type where game_type.type_name in (“RPG”,“MMORPG”) LIMIT 2会产生临时表,这张临时表名称就是<derived2>,第一行结果所示,其中derived后面的2就是上面第三行结果的id值,表示这是表game_type查询产生的临时表。

  • UNION:若第二个select语句出现在UNION之后,则被标记为UNION。若UNION包含在from子句的子查询中,外层select则被标记为DERIVED

    上图中SQL语句中,union后面的select语句,特意在right join game_company后面加上as company,所以在explain结果中能明显看出select_type=UNION的table分别是company和game,说明了union后面的select语句会被标记为UNION。

  • UNION RESULT:从union表获取结果的select

    union上下的查询语句会把结果合并到临时表<union1,2>中,结果会从这张表中返回。

explain输出列-type

  • system:表只有一行记录,这是const类型的特例

    上图所示from列表子查询只有一行记录,其派生出的临时表<derived2>只有一行记录,因此外层临时表的select查询的type值就是system

  • const:表最多有一个匹配行,它将在查询开始时被读取

    还是上图,from列表子查询的where条件是主键=常量,所以肯定只有一行记录被匹配,所以type的值为const

    上图所示第一条SQL语句查询结果只有一条记录,但是下面explain的结果显示type值却是ref。Mysql优化器发现查询条件里面的主键或者唯一索引是个常量值,所以判断肯定最多只有一行记录匹配,因此采用type=const的查询方式,即只需要读取到匹配的第一行记录即可返回;而并不是系统已经返回结果了发现只有一行记录,因而才显示type=const。所以例子1是const而例子2是ref。在这一点上Mysql是兵马未动而粮草先行,而并非事后诸葛亮。

  • eq_ref:驱动表和关联表中的每行进行组合并且仅有一行记录

    以表game为驱动表关联表game_type的时候,表game和表game_type是一对一关系(game_type.id是主键),一个游戏只能是一种游戏类型,所以表game的每一行记录都只能和关联表game_type组合成最多一条记录,所以在关联查询表game_type时,type是eq_ref,表示驱动表每行记录只需要和关联表组合到第一行记录即可。

  • ref:非唯一性索引扫描,返回符合某个索引值的所有记录,可能会有多条记录匹配

    单表查询,表game的type_id字段是普通索引,所以优化器判断其可能会有多条匹配的记录,所以type=ref,会读取所有匹配的记录,而不会像const一样碰到第一行记录匹配便返回。

    联表查询,以表game_type为驱动表的时候,因为game.type_id不是主键也不是唯一索引,所以系统判断表game_type和表game存在多对一的关系,因此表game_type的每一行记录都可能和关联表game组合成多行记录,所以在关联查询表game时,type=ref,表示驱动表每行都会组合匹配到最后。

  • range:使用一个索引来检索给定范围的行,这种范围索引扫描比全表扫描效率要高。

  • index:索引树扫描

    select列表只包含id,name两个字段,而所以索引Uk_name就包含这两列,所以Mysql只需要扫描索引树既可以找到所需要的所有值,不需要去扫描表。

    tips: 按照Mysql索引最左原则,"name like %争霸%"是无法使用索引列name的,但这是模糊搜索常常出现的需求,所以我们可以像上面语句一样通过扫描索引树找出所需要的Id(最好配合分页),再通过这些id去查询相应的行记录。索引树存在内存里面,就是全索引树扫描也比扫描全表要快的多得多。

  • all:全表扫描

explain输出列-possiable_keys、key

  • possiable_keys:可能会使用到的索引;key:实际会使用到的索引。

    possiable_keys显示可能会使用的索引有两个:uk_name,ik_name_typeId,而key显示实际用到的索引是uk_name。

    如果查询用到的是覆盖索引,并且没有where查询条件,key会显示实际用到的索引,但是possiable_keys不会显示。

explain输出列-key_len

  • key_len:索引字节长度,可以分为变长和定长数据类型两种。当索引字段为定长数据类型时,如char,int,datetime,需要有是否为空的标记,这个标记占用1个字节(对于not null的字段来说,则不需要这1字节);对于变长数据类型,比如varchar,除了是否为空的标记外,还需要有长度信息,需要占用两个字节。

    索引uk_name只有一个索引列name,其中name类型是varchar(255),字节编码utf-8mb4,因此key_len=255 * 4 + 2 +1=1023。

    索引ik_name_typeId包含两个索引列type_id(int)和name(varchar(255)),字节编码utf-8mb4,所以key_len=(255 * 4 + 2 + 1) + (4 + 1) =1028。

explain输出列-ref

  • ref:哪些列或者常量被用做索引列上的值

    type_id=1,1是常量,ref=const

    表game被关联查询的时候,使用了ik_typeId索引,索引列是game.type_id,而值则使用了驱动表的game_type.id列。

explain输出列-rows

  • rows:根据表的统计信息和索引的使用情况,大致估算查询结果所需要读取记录的行数

    全表扫描,返回结果大概需要扫描21行记录。

    索引范围扫描,返回结果大概需要读取2行记录。

explain输出列-extra

  • using filesort:使用文件内排序
    在这里插入图片描述
    因为查询使用了索引ik_typeId,该索引只有type_id一列,因此无法使用索引来完成order by name的排序,需要额外进行外部的排序动作,这一般是不能容忍的,需要优化。
    上图就是使用了ik_typeId_name索引,能直接利用索引完成order by type_id,name的排序,因此extra输出列不会出using filesort。

    tips:这里简单提一下第一个例子的优化问题
    一、例子1不是索引不完整造成,而是索引顺序问题,所以首先可以从程序实现上或者需求上考虑,能不能像例子2一样去调整sql语句,让where和order by都用上索引,例子2是调整了order by;同样也可以调整where,比如type=1,让type等于一个常量值也可以让现有的索引同时满足where和order by。
    二、但是说如果你的程序实现无发调整而且需求上就是要type_id是范围查询,order by的第一字段就是name,那你可以考虑一下使用覆盖索引,先查询到刷选并且排序后的Id列表(最好配合分页),再通过id列表去读取完整的行数据,虽然也是需要额外对索引树的扫描结果进行排序,因此using filesort还是会存在,但是这些全部都是在内存中完成,还是比读取源表数据后再排序要快上很多的,尤其是数据量大的时候,主要减少了IO消耗。

  • using tmporary:使用临时表保存中间结果,常见于排序order by和分组group by。

    从possiable_keys列结果中,我们能发现并没有包含company_id列的索引,所以where条件使用ik_typeId索引,但是group by并没有索引可以使用,因此使用了临时表进行分组。

    这是优化后的结果,从possiable_keys列结果中可以发现可用索引多了一个ik_companyId_type_id,而key列结果中也显示了实际上也是使用了这个索引,因此group by可以依靠索引进行分组,不再需要使用临时表。

  • Using index:表示覆盖索引即可满足查询要求,因而无需再回表查询

  • Using where:表示MySQL将对存储引擎层提取的结果进行过滤,它表示的是Server层对存储引擎层返回的数据所做的过滤。

    上面查询使用了索引ik_companyId_typeId,索引在引擎层只能通过索引完成对company_id=1的刷选,而name like '魔兽%'只能在读取源表后,在Mysql Server层进行第二次刷选。

  • Using index;Using where:使用了覆盖索引读取数据,并且利用索引进行查找。

    这种还是直接使用了覆盖索引的数据,只是对结果进行了一次过滤,不需要读取源表因此查询效率还是非常高的!

  • Using index condition:使用了Index Condition Pushdown (IPC) ,这是Mysql 5.6开始支持的一种根据索引进行查询的优化方式。其优化支持range、ref、eq_ref、ref_or_null类型的查询,当前支持MyISAM和InnoDB存储引擎。

    key列的结果显示,实际使用的索引是ik_companyId_typeId,Mysql可以根据索引来获取company_id between 1 and 3的记录,但是对于type_id < 4却无能为力。若不支持IPC,则数据库需要先通过索引列company_id获取区间[1,3]中的所有记录,然后再过滤type_id<4这个条件,如果满足第二个条件会过滤掉非常多的数据,那样读取源表记录时包含非常多无用数据,影响查询效率。若支持IPC,满足第一个where条件的索引在取出的时候,会利用该索引树,直接进行其他where条件的过滤,最后再利用过滤后的索引去读取源表的完整记录。IPC的情形是无法直接通过读取索引满足所有where条件,但是索引又包含where的所有条件列,Ik_companyId_typeId有包含了company_id和type_id两列,所以符合IPC的情形。

    上面图中SQL语句强行使用了索引Ik_companyId,发现extra的值时Using where。IPC的使用条件情形是,索引扫描无法直接满足where的所有条件,但是该索引又包含所有where条件列。但是索引ik_companyId缺少type_id列,所以无法满足IPC的情形,因此会读取满足第一个条件的所有记录,再在Sql层进行where过滤。

  • Using index for group by:数据访问和 Using index 一样,所需数据只须要读取索引,当查询 中使用GROUP BY或DISTINCT 子句时,如果分组字段也在索引中,Extra中的信息就会是Using index for group-by

  • Using join buffer:

    Block Nested-Loop Join算法:将外层循环的行/结果集存入join buffer, 内层循环的每一行与整个buffer中的记录做比较,从而减少内层循环的次数。优化器管理参数optimizer_switch中中的block_nested_loop参数控制着BNL是否被用于优化器。默认条件下是开启,若果设置为off,优化器在选择 join方式的时候会选择NLJ(Nested Loop Join)算法。
    Batched Key Access原理:对于多表join语句,当MySQL使用索引访问第二个join表的时候,使用一个join buffer来收集第一个操作对象生成的相关列值。BKA构建好key后,批量传给引擎层做索引查找。key是通过MRR接口提交给引擎的(mrr目的是较正顺序)MRR使得查询更有效率,要使用BKA,必须调整系统参数optimizer_switch的值,batched_key_access设置为on,因为BKA使用了MRR,因此也要打开MRR。

  • impossiable where:where的值总fasle,不能获取任何记录

  • Select tables optimized away:在没有group by子句的情况下,基于索引优化的MAX/MIN操作,或者基于MyISAM存储引擎优化的COUNT(*)操作,不必等到执行阶段再进行计算,在查询计划生成阶段既可以完成优化。

  • Distinct:优化Distinct操作,在找到匹配的第一行记录后,立马停止查找同样的值。

    上面语句查找所有开发过type_id=2的类型游戏的游戏公司id,如果表game里面有两条company_id和type_id组合一致的记录,比如暴雪公司开发过两款RTS游戏星际争霸和魔兽争霸,那对于内层关联查询,只需要匹配到第一条记录便可以停止查询这个组合。

参考博客

MySQL中explain执行计划中额外信息字段(Extra)详解

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!