Explain介绍
Explain是mysql自带的一个工具,我们不用额外做什么配置即可使用。附上mysql关于explain的官方文档。通常一个sql语句执行的大概流程是由连接器-分析器-优化器-执行器这么一个过程。
Explain就是模拟优化器这个阶段去执行SQL语句,分析查询语句或是结构的性能瓶颈。在 select 语句之前增加 explain 关键字,MySQL 会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL。哪怕from 中包含子查询,仍会模拟执行该子查询,将结果放入临时表中。
数据准备
为了更好说明Explain准备了几张表及数据加以说明。
演员表除了主键索引,没有其他索引,而电影表还有一个name的普通索引,电影演员关系表还有film_id actor_id联合索引。至此,表、索引和数据准备完毕。了解Explain,先从了解Explain中的各个列开始。
explain中的列
id列
id是查询语句的编号,有几个 查询语句就有几个id,id的顺序是按 select关键字 出现的顺序增长的,id值越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。
id=3 是from 后面的子查询最优先执行,id=2是select语句中子查询第二优先,id=1的select最后执行。但是呢它们的序号是根据select 关键字出现的顺序来决定的。
select_type列
select列有下面几种值:
1.simple:简单查询,查询不包含子查询和union.。
2.primary:复杂查询中最外层那个的查询是主要查询。
3.subquery:在select中的子查询,不是在 from子句中。
4.derived:在from子句中的子查询,会将结果存放在一个临时表中,也称为派生表。
table列
表示当前这一条正在查询哪张表。正常的表都好理解如上图的id=3正在查询film表,id=2正在查询actor表。派生表是这样表示的,from 子句中有子查询时,table列是 <derivenN> 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。
id=1的查询查的表示<derived3>表,这是一张临时表,依赖select_type为DERIVED的并且id为3的查询。
type列
表示访问类型,就是MySQL决定如何查找表中的行,查找数据行记录的大概范围。依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL。一般来说,得保证查询达到range级别,最好达到ref。
1.NULL:mysql能够在优化阶段分解查询语句,在执行阶段用不着再访问表。
直接在索引树中选取最小的索引值即可,可以单独查找索引来完成,不需要在执行时访问表所以就为null,这个性能是非常高的。
2.const, system:mysql能对查询的某部分进行优化并将其转化成一个常量(可以看show warnings 的结果)。通常是用于primary key 或 unique key 的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。system是const的特例,表里只有一条元组匹配时为system。
from子查询中(SELECT * FROM film WHERE id = 1)为什么是const级别的呢。通过show warnings可以看出/* select#1 */ select '1' AS `id`,'film1' AS `name` from dual
因为他是主键查询(或唯一索引)mysql将这条语句优化成常量来处理,也就是说直接拿到id=1的数据作为常量,说白了查起来效率非常高跟拿常量似的。另一条为什么是system呢,是因为从常量里面拿数据,而这个常量刚好只有一条数据的时候。
3.eq_ref:primary key 或 unique key 索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录。这可能是在const 之外最好的联接类型了,基本上不用做优化。简单的 select 查询不会出现这种 type。
在表连接查询的时候,如果用到的是主键关联就是这种eq_ref类型,对film表来讲是用主键ID关联的,一个主键ID只会明确一条记录。
4.ref:相比 eq_ref,没有使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。简单点理解是用到了普通索引。
film_actor表示电影演员关系表,有一个联合索引film_id,actor_id这里用到了左边前缀film_id部分。索引的前缀部分查出来也是有可能多条的,所以一般都是ref。在都有索引的情况下,与eq_ref相比区别就是是否能确定唯一。
5.range:用到了索引,主键索引,普通索引,唯一索引都有可能,但是呢,它是针对范围扫描的。通常出现在 in(), between ,> ,<, >= 等操作中,使用一个索引来检索给定范围的行。简单点理解就是通过索引进行范围查询。
range效率一般还是可以的,因为它还是会走索引的,但是范围太大的话效率也不会高。
6.index:不要被index这个单词给欺骗了,以为是有走索引。它确实跟索引有关但不是通过索引树去查找,而是扫描全索引的叶子节点,其实是另外一种的全部扫描,只不过它扫全部的索引叶子节点,而不是全部表(上面说的几个类型,都是从树顶开始二分查找快速定位到)。所以会比全表扫描好一点。通常这种情况是扫描某个普通索引,毕竟普通索引的数据量会小一点。这种查询一般是使用了覆盖索引(覆盖索引是逻辑上的索引不是物理存在的),也就是说select fied1,filed2 这些字段刚好都在索引上所以扫描能拿到值不需要在回表等。
因为film表只有两个字段,id和name,在name的索引树上就能拿到所有的数据,所以通过扫name索引的叶子节点就能拿到数据。这也是这条语句没有走all全表扫描的原因。也就是说查询的字段在二级索引上能全部覆盖的到,主键索引也能覆盖的到,明显二级索引的数据量会更少。
7.ALL:全表扫描,扫描聚集索引(通常是主键索引)的所有叶子节点。聚集索引的叶子节点就是整张表的数据所以就是全表扫描。通常情况下这需要增加索引来进行优化了。
actor表只有一个主键索引,没有二级索引,就算有二级索引也不一定会覆盖到查询的所有字段。
简单的讲system,const,eq_ref都是和主键索引或唯一索引有关,ref和二级索引有关(普通索引,联合索引等),range和索引(主键索引或二级索引)加范围有关,index是扫描二级索引叶子节点,all是扫描聚集索引叶子节点(直接扫描叶子节点性能很低,没有用到树的从树顶开始二分查找方式)。
possible_keys列和key列
通常要将这两个值放在一起看,prossible_keys表示查询的时候可能会用到哪些索引来查找,key表示查询的时候用到的索引。注意可能用到的索引只是mysql的预估,但是最终选择用或不用或用哪个(这一块主要是mysql优化器里面会去判断用最小的代价,暂且不表)是要看key的值。
比如:
1.出现 possible_keys有值,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,mysql认为索引对此查询帮助不大,选择了全表查询还更快。
2.possible_keys列值是NULL,说明没有相关的索引或不会用到索引。在这种情况下,可以通过检查 where 子句看是否可以创造一个适当的索引来提高查询性能,然后用 explain 查看效果。
如果想强制mysql使用possible_keys列中的索引,可以在查询中使用 force index,但是不建议这样做因为mysql底层机制的选择通常是对的。
key_len列
表示mysql在索引里使用的字节数,一般主键索引,唯一索引,单个字段的普通索引不用太关心这个值,有走索引(上面提到的key列有值)就是有值没有走索引就是没有值。联合索引可以关注这个值,上面的key列有值的时候,key_len列肯定也是有值的,就看key_len的长度是多少来判断用到了联合索引中的几个字段。
key_len计算规则
1.字符串,char(n)和varchar(n),n是代表字符数,而不是字节数,如果是utf-8,一个数字或字母占1个字节,一个汉字占3个字节。
char(n):如果存汉字长度就是 3n 字节。
varchar(n):如果存汉字则长度是 3n + 2 字节,多的2字节用来存储字符串长度,因为varchar是变长字符串需要记录长度。
2.数值类型
tinyint:1字节
smallint:2字节
int:4字节
bigint:8字节
3.时间类型
date:3字节
timestamp:4字节
datetime:8字节
4.如果字段允许为 NULL,需要1字节记录是否为 NULL
一般情况索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。
ref列
表示在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:film.id)
rows列
该列是mysql预估要读取并检测的行数,注意这个不是结果集里的行数只是一个预估值。
Extra列
表示额外信息。常见的重要值如下:
1.Using index:使用覆盖索引
覆盖索引定义:覆盖索引不是物理存在的索引,是逻辑上的一个概念。mysql执行计划explain结果里的key有使用索引,如果select后面查询的字段都可以从这个索引的树中获取,这种情况一般可以说是用到了覆盖索引,extra里一般都有using index;覆盖索引一般针对的是二级索引,整个查询结果只通过二级索引就能拿到结果,不需要通过索引树找到主键,再通过主键去主键索引树里获取其它字段值。
2.Using where:就是一个普通的where语句查询且没有用到索引。这种情况,key列一般为null值。
3.Using index condition:查询的列不完全被索引覆盖,where条件中是一个前导列的范围。联合索引的时候比较常见,联合索引a,b两个字段,只用到了a。
4.Using temporary:mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索引来优化。
跟file表对比,因为film表name有索引,遍历索引的时候就能把重复的过滤掉,所以不需要临时表。而actor表需要将结果放临时表里在去重。
5.Using filesort:将用文件排序而不是索引排序,数据较小时从内存排序,否则需要在磁盘完成排序。这种情况下一般也是要考虑使用索引来优化的。
第二条name有索引,直接在索引树就能排好序,而第一条name没有索引,基本上都要先查出来放内存,数据大的话可能会放文件在排序效率明显就差。
6.Select tables optimized away:使用某些聚合函数(比如 max、min)来访问存在索引的某个字段,这种大多数没有走到索引。比如:explain select min(id) from film;
以上把Explain的主要信息都介绍了一遍,下面通过几个查询语句优化的例子来加深对Explain的印象。
优化实践
重新准备一个案例,来做一点Explain分析和优化来彻底搞懂Explain。开始前做点数据准备。
全值匹配
这三条语句都符合我们的最左前缀原则,执行计划也都差不多。他们的主要区别就是索引用到的字段个数不同,第一条就用了1个,第二条用了2个,第三条用了3个。通过key_len可以知道是否都用上联合索引的所有字段(key_len计算规则见上面),全值匹配就是指联合索引的所有字段都有用上,效率会更高。因为它的区分度更高,范围更精确。
最左前缀法则
如果索引了多列,要遵守最左前缀法则,查询从索引的最左前列开始并且不跳过索引中的列。
明显就第一句是有走索引的。
不在索引列上做任何操作(计算、函数、(自动or手动)类型转换)
否则,会导致索引失效而转向全表扫描
从索引树结构就可以知道,name的某几位,在索引树里面是没有这些东西的,如何走索引,而且某几位的截取顺序都乱了也肯定走不了索引。虽然例子中用的是left函数,但是mysql不针对某个函数特殊处理,用函数都认为是无序的。
不能使用索引中范围条件右边的列
第一条全值匹配没问题,第二条,第一个条件=lilei那边第二,第三个字段肯定都是有序的,但是呢,从第二个条件开始是大于22,那么第三个字段就不一定有序了所以第二条语句只用到两个字段,key_len=78。
尽量使用覆盖索引
在工作中就是尽量少用select *,只查询需要的字段,而这些字段刚好都在二级索引树上,避免回表。
两条语句都用到了索引,而且索引的字段和长度都一样,但是extra不同,第二条语句由于是select * 还要进行一次回表操作,而第一条语句只查询name,age而这两个字段二级索引树上都有不要回表。
用不等于(!=或者<>),not in ,not exists 的时候无法使用索引会导致全表扫描
虽然prossible_keys有值,但是key没有值,也就是说可能会用到,但实际就是没有用到。
is null,is not null 一般情况下也无法使用索引
虽然查的是name 是索引字段,但是实际就是没有查索引,null值跟其他值有一个指针关系,索引也不知道指针位置该如何顺序定位,说白也就是无序。
like以通配符开头
第一句like是通配符打头不走索引,因为%通配符可能是任意字符是无序的。第二句通配符在后面以固定的字符开头查询前面的字符是有序的能走索引。第三句跟第二句比没有用select *,就是只查二级索引上有的数据所以用到了覆盖索引。
类型不匹配索引失效
name是字符串类型,第二句没有加引号,后台会加个函数转字符串,用函数mysql就索引失效,type=all全表扫描。
少用or或in
mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。mysql认为数据量小扫描还更快。
索引优化总结
相信到这里,通过介绍Explain各属性及其结合优化案例的使用,对它会有一个比较好的理解了。最后附上一张使用总结图。