0
点赞
收藏
分享

微信扫一扫

mysql深度剖析二(根据explain拿到执行计划进行优化)

waaagh 2022-04-06 阅读 95
mysqljava

根据explain拿到执行计划进行优化

id

有几个select就有几个id,越大越先执行,相等时从上往下执行

select_type

simple:简单查询。查询不包含子查询和union;
primary:复杂查询中最外层的 select;
subquery:包含在 select 中的子查询(不在 from 子句中);
derived:包含在 from 子句中的子查询。MySQL会将结果存放在一个临时表中,也称为派生表(derived的英文含义);
union:在 union 中的第二个和随后的 select;

table列

这一列表示 explain 的一行正在访问哪个表。
当 from 子句中有子查询时,table列是 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。当有 union 时,UNION RESULT 的 table 列的值为<union1,2>,1和2表示参与 union 的 select 行id。

type字段

一共有8种分为4组情况:
system与const:前者是后者的特例,前者有且只有一行数据,后者用到了主键索引查到了唯一 一行数据;
eq_ref与ref:前者使用了主键或唯一索引进行关联查询,最多只会返回一条符合条件的记录,简单查询不会出现这种;后者使用普通索引或者在关联查询中使用了联合索引的最左前缀法则;
range:用索引进行了范围查询
index与all:前者指只需扫描二级索引中所有叶子节点就能拿到所需数据,即全索引扫描,后者指需要扫描聚集索引中所有叶子节点才能拿到所需的完整数据,即全表扫描。一般加了where条件,会从索引根节点按照二分法开始查找。index比all效率更高一点,因为二级索引的叶子节点比聚集索引的叶子节点包含的数据少,磁盘io效率更高。
null:不用访问用户表;

extra字段

该字段下可能存在几十种情况,但大多数情况很难碰到,并且extra的结果不是很准确,仅作为参考,这里只分析常见情况:
using index:使用覆盖索引,可以这么理解:用二级索引树就可以搞定了;
using index condition :使用了索引下推
using temporary:对于select distinct xxx …的操作,若xxx字段没有用到索引,则会先建一张临时表,然后再去重,所以优化办法是对xxx字段加索引,此时就会变成using index,即一边从左到右扫描二级索引的叶子节点一边去重,避免了建立临时表,提高了效率;
using filesort:使用了外部排序而没利用索引树叶子节点进行排序,这里有个单路排序和双路排序的概念,单路排序指利用二级索引找到全部数据,再进行排序;双路排序指先根据where条件后的几个字段进行排序,再回表查出完整数据。比如order by xxx,当没有给xxx字段加索引时,会走using fileSort,即在内存或者在磁盘排好序(数据量较大时),当给xxx字段加上索引时,会走using index,即利用二级索引的叶子节点本身就具有顺序的特征拿到数据;
单路排序有点类似于索引下推,先过滤到符合要求数据的所有字段,再排序,而双路排序则是先过滤到符合要求的排序字段和可以直接定位到数据行的id,接着排序,排好序后,再根据id回表取回其他字段;
所以单路排序占用内存大,但排好序后,结果就出来了;双路排序占用内存小,但排完序后,还要根据id回表查出所有数据;
在这里插入图片描述

using where :没有使用索引

rows

扫描的行越多不代表查询耗时长;扫描的行数少,但回表次数多,也会导致耗时长;扫描行数多,但不回表,耗时也会短些。
关闭查询缓存
set global query_cache_size=0;
set global query_cache_type=0;

执行时间0.333s(扫描行数多,但不回表),走的是全表扫描
SELECT * FROM employees WHERE name > ‘LiLei’;

执行时间0.444s(扫描行数少,但回表了),走的是二级索引,然后回表
SELECT * FROM employees force index(idx_name_age_position) WHERE name > ‘LiLei’;
mysql内部可能觉得第一个字段就用范围,结果集应该很大,回表效率不高,还不
如就全表扫描;

possible_keys与key

可能会用到的索引与实际用到的索引

key_len

在mysql建表定义字段时,每个字段所属类型都有一定的长度,如int类型是4字节,varchar(n)长度为3n+2,char(n)为3n等等,所以可以根据key_len的长度反推出来到底是否使用了某些索引;

索引优化

为啥对索引字段使用了函数后,索引会失效呢

因为使用了函数后,在索引树上找不到该值了;

数据量较少可能宁愿全表扫描也不回表

在这里插入图片描述

虽然上述红圈中的sql针对上一条使用函数导致索引失效已做了改进,但mysql会判断如果进行回表效率比全表扫描更低,那么即使where条件中用到了普通索引,也不会走,而直接进行全表扫描。所以运维扫描sql说用了全表扫描的需要整改,但已经加了索引为啥还会全表扫描呢,这个就是原因。所以使用二级索引进行查找,mysql内部优化器会根据检索比例,表数据量等因素决定是否走二级索引。
数据量较少时,如果全表扫描比较快,就不会走回表;
数据量比较多时,如果回表快,就不会走全表扫描;所以是否走索引,也和表的数据量有关;
数据量比较多时,如果全表扫描快,就不会走回表,从而放弃走索引;尽管此时全表扫描的行数比较多,但相比于回表,前者性能更高;
所以是否走索引,mysql内部会根据检索比例,表大小等多个因素整体评估;mysql内部有一个时间成本计算;
使用in,or时,和使用like时不同,使用in,or时,会遵从上边的结论,但使用like时,如like xxx%,则不管表多小,都会走索引;

同一张表的同一个sql也不一定每次都走索引

在这里插入图片描述

employee表有10万行数据,但第一条语句不走索引,第二条语句走索引;为啥会这样呢?通过trace工具可以知道到底为啥要选择走索引或不走;
开启trace工具后,先后执行如下select;
在这里插入图片描述

得到TRACE字段中的内容即是分析的过程,这是json格式,找到rows_estimation即预估表的访问成本,其中会进行一下判断:
预估全表扫描的行数rows,时间成本cost;
预估主键索引的时间成本cost;
预估辅助索引的时间成本cost,可能包括扫描,回表等等,如果cost值较大,则chosen会为false,即不选择该索引;
最终cost少的,为最优选择;
以上均为预估,并不是要将sql执行一遍,再去判断;

强制走索引

在这里插入图片描述
在这里插入图片描述

如果强制走索引比mysql优化器内部优化(内部优化没有走索引)的效果更好,则可以执行强制索引;

联合索引加了范围查找会导致右边的索引失效

如(a,b,c)为联合索引,where条件先精确匹配a,对b使用范围查找,此时c就无法精确匹配,因为b使用范围查找后,c是个无序状态,无法精确匹配;索引能被使用的前提一定是已经排好序的数据结构。

如果用了全表扫描就往全索引扫描上优化

select后的字段是二级索引叶子叶子节点中可以拿到的字段

trace工具

可以看到选择索引的判断过程

索引下推

在这里插入图片描述

Mysql5.6之前没用到索引下推时,二级索引中查到like条件后,就立即回表,拿到完整数据,再回过头来按照age,position条件进行过滤;5.6版本后,二级索引中每个条件都要用上,缩小了结果范围,最后才回表,减少了回表次数,即索引下推可以这么理解:根据name过滤了一遍后,还会根据age,position索引继续推断一下,最后得到一些主键id,再去主键id的索引树中找;
不一定是like,其他情况也有可能会用到索引下推;
索引下推可以手动关闭,但索引下推通常可以提高性能,所以一般不关闭;
大于通常不使用索引下推,因为大于得到的结果集比较大,而like得到的结果集比较小;
这个地方其实也有点模棱两可,只有研读mysql的源码,才能真正搞懂这些逻辑;

联合索引中like,范围查找,in,or的区别

in或者or在表数据量小时一般会走全表扫描,在表数据量大时,会走索引;
like无论表的数据量多少,都会走索引,而且是基于索引下推;
范围查找不会走索引下推,因为范围查找得到的结果集比like大,所以在二级索引上再次过滤可能会很耗时;

order by与group by

如新建一个联合索引(name,age,position),order by后使用的字段一样也要遵从最左前缀法则;另外使用覆盖索引也一样有效果;

select * from xxx where name>’a’ order by name;
修改成
select name,age,position from xxx where name>’a’ order by name;
此时使用explain观察执行计划,发现type由all 变为了range,extra由using fileSort变为了using index,利用覆盖索引,无需回表,并且顺序直接通过在二级索引树的叶子节点就可得到;
在这里插入图片描述

limit

使用limit 1000,5;其实过程是一共查了第1条到第1005条记录,最后删除了前1000行;
在这里插入图片描述

最后一行是优化的sql,join后的子查询使用了覆盖索引查询了第1条到第90005条记录,最后删除了前90000条,返回一个较小的结果集5条记录,此时再用较小的结果集去关联大表可以查到所有字段,避免了上边的全表扫描从第1条到第90005条记录,效率明显提升;执行计划分别如下:
在这里插入图片描述
在这里插入图片描述

inner join
在这里插入图片描述

1)当字段a有索引时,上边sql的执行计划如下:
在这里插入图片描述

2)当字段a没有索引时,上边sql的执行计划如下:
在这里插入图片描述
在这里插入图片描述

嵌套循环连接算法与基于块的嵌套循环连接算法

若有两张表t1,t2,分别有10000数据和100条数据,sql是
select * from t1 inner join t2 where t1.a = t2.a;
1)当字段a建立了索引,基于嵌套循环连接算法时,执行过程是这样的:取出t2中的每一行数据中的字段a,总共会有100次磁盘扫描,去t1表的字段a的索引树中找,由于索引寻找会很快,索引这里又是100次磁盘扫描,所以加起来一共有200次磁盘扫描;
2)当字段a没有建立索引,基于嵌套循环连接算法时,执行过程是这样的:取出t2中的每一行数据中的字段a,总共会有100次磁盘扫描,由于a字段并未建立索引,此时会对t1表进行磁盘扫描10000次,所以总共是100乘以10000,即100万次磁盘扫描;
3)当字段a没有建立索引,基于块的嵌套循环连接算法时,执行过程是这样的:将t2中的100行数据加载进内存join_buffer中,总共100次磁盘扫描,接着对t1表的每一行取出来和join_buffer中的数据进行比对,又是10000次磁盘扫描,总共就是10100次磁盘扫描,但是在内存中需要判断100万次,但100万次内存中的比对远远比100万次磁盘扫描效率高;
所以上述1),2),3)三种情况,最终性能是1)> 3)> 2);

dateTime与timestamp的区别

在这里插入图片描述

优化思路

1)写完业务代码,写完所有sql后,再集中对所有sql进行处理,看看哪些字段需要加索引,而不是建表建索引一起弄;
2)首先设置整型自增id的主键索引;
3)接着看哪些字段需要作为查询条件,如果作为条件的字段很少,就对每个字段设置索引,注意使用了函数的字段索引会失效,即不要对字段包装后再进行条件比较,注意把包装拆开;order by后的字段注意要使用到索引;select *少用,对于二级索引可以减少回表;离散度小的字段不益建索引;where和order by中使用的字段一起构成最左前缀原则;
4)如果作为条件的字段较多,则可以考虑设置联合索引,要注意几点
4.1)最左前缀原则;
4.2)注意使用覆盖索引,即查询的字段都属于联合索引字段,而不是select *,前者可以不回表;
4.3)设置联合索引时,注意联合索引第一个字段用范围不会走索引,mysql内部可能觉得第一个字段就用范围,结果集应该很大,回表效率不高,还不如就全表扫描;
4.4)in和or在表数据量比较大的情况会走索引,在表记录不多的情况下会选择全表扫描; 4.5)like KK% 一般情况都会走索引,如果后边还有条件,很可能会用到索引下推;
4.6)order by满足两种情况会使用Using index,order by语句使用索引最左前列,使用where子句与order by子句条件列组合满足索引最左前列;
5)加好索引后,使用explain查看执行计划,检查type字段是否达到了range及以上,如果是全表扫描,看能否使用覆盖索引,变成全索引扫描;检查extra字段提示使用的哪种方式,是否达到预期,如果是using filesort看是否是由于where 与 order by的条件组合顺序没有按最左前缀的顺序;
6)清掉缓存, set global query_cache_size=0,set global query_cache_type=0; 比较不同索引字段下sql的执行时间,或者使用trace工具查看具体成本。
7)查看慢sql日志,哪些sql耗时过长,进一步分析处理。
8)索引优化举例:
select * from employees ORDER BY name limit 90000,5;
直接进行全表扫描了,并没有利用name字段所在的二级索引;
改成如下:
select * from employees e inner join (select id from employees order by name limit 90000,5) ed on e.id = ed.id; 变成了全索引扫描,避免了回表,性能翻倍;即使用普通索引作为where条件,若没有走普通索引,而走的是全表扫描,则考虑使用覆盖索引,找到主键id,再用该id作为条件去主键索引树中找;

sql优化举例

1)先根据业务场景写出sql;
2)看sql中where,order by的条件是哪些字段,这些字段组合成一个联合索引;若where中存在范围查询,导致order by中的字段无法走索引,此时优先where走索引;
3)若where中遇到了范围查找,则调整联合索引字段的顺序,将范围查找的字段放在联合索引的最后边;
4)若联合索引中跳过了某些字段即这些字段没有经常被作为条件,且被跳过的字段是有基数很小,则直接用in,把范围罗列出来;
5)若联合索引中有多个字段需要进行大于或者小于的范围查询,则看是否能将这种范围查询变为等于,不等于这样的,这样就可以继续走联合索引;
6)若一个联合索引无法解决问题,再新建一个联合索引,一张表通常可以建2到3个联合索引;但联合索引建得多了,会影响性能,因为在插入,删除时,联合索引树需要不断变换;如果是读多写少的场景,则联合索引建3到4个可以,若是读多写多的场景,则联合索引建1到2个可以;

在这里插入图片描述

举报

相关推荐

0 条评论