sql查询调优之where条件排序字段以及limit使用索引的奥秘

2018-11-26 17:05:47

奇怪的慢sql

我们先来看2条sql

第一条:

select * from acct_trans_log WHERE  acct_id = 1000000000009000757 order by create_time desc limit 0,10

  

第二条:

 select * from acct_trans_log WHERE  acct_id = 1000000000009003061 order by create_time desc limit 0,10

表的索引及数据总情况:

 

索引:acct_id,create_time分别是单列索引,数据库总数据为500w

通过acct_id过滤出来的结果集在1w条左右

 

查询结果:第一条要5.018s,第二条0.016s

为什么会是这样的结果呢?第一,acct_id和create_time都有索引,不应该出现5s查询时间这么慢啊

 

那么先来看执行计划

第一条sql执行计划:

 第二条执行计划:

 仔细观察会发现,索引只使用了idx_create_time,没有用到idx_acct_id

这能解释第一条sql很慢,因为where查询未用到索引,那么第二条为什么这么快?

看起来匪夷所思,其实搞清楚mysql查询的原理之后,其实很简单

 

我们来看这2条sql查询,都用到了where order by limit

当有limit存在时,查询的顺序就有可能发生变化,这时并不是从数据库中先通过where过滤再排序再limit

因为如果这样的话,从500万数据中通过where过滤就不会是5s了。

 

 

此时的执行顺序是,先根据idx_create_time索引树,从最右侧叶子节点,反序取出n条,然后逐条去跟where条件匹配

若匹配上,则得出一条数据,直至取满10条为止,为什么第二条sql要快,因为运气好,刚好时间倒序的前几条就全部满足了。

 

搞清楚原理之后,我们了解了为什么第一条慢,第二条快的原因,但是问题又来了

为什么mysql不用idx_acct_id索引,这是一个问题,因为这样的话,我们的建立的索引基本失效了,在此类sql下

查询效率将会是相当低

 

因为通过acct_id过滤出来的结果集比较大,有上万条,mysql认为按时间排序如果不用索引,将会是filesort,这样会很慢,而又不能2个索引都用上

,所以选择了idx_create_time。

 

为什么mysql只用一个索引

这里为什么不能2个索引都用上,可能很多人也不知道为什么,其实道理很简单,每个索引在数据库中都是一个索引树,其数据节点存储了指向实际

数据的指针,如果用一个索引来查询,其原理就是从索引树上去检索,并获得这些指针,然后去取出数据,试想,如果你通过一个索引,得到过滤后的指针,这时,你的另一个条件索引如果再过滤一遍,将得到2组指针的集合,如果这时候取交集,未必就很快,因为如果每个集合都很大的话,取交集的时候,等于扫描2个集合,效率会很低,所以没法用2个索引。当然有时候mysql会考虑临时建立一个联合索引,将2个索引联合起来用,但是并不是每种情况都能奏效,同样的道理,用一个索引检索出结果集之后,排序时,也无法用上另一个索引了。

 

实际上用索引idx_acct_id大多数情况还是要比用索引idx_create_time要快,我们举个例子:

select * from acct_trans_log force index(idx_acct_id) WHERE  acct_id = 1000000000009000757 order by create_time desc limit 0,10

耗时:0.057s

可以看出改情况用idx_acct_id索引是比较快的,那么是不是这样就可以了呢,排序未用上索引,始终是有隐患的。

 

 

联合索引让where和排序字段同时用上索引

我们来看下一条sql:

select * from acct_trans_log force index(idx_acct_id) WHERE  acct_id = 3095  order by create_time desc limit 0,10

耗时: 1.999s

执行计划:

 该sql通过acct_id过滤出来的结果集有100万条,因此排序将会耗时较高,所幸这里只是取出前10条最大的然后排序

查询概况,我们发现时间基本消耗在排序上,其实这是内存排序,对内存消耗是很高的。

 

 那么我们有没有其它解决方案呢,这种sql是我们最常见的,如果处理不好,在大数据量的情况下,耗时以及对数据库资源的消耗都很高

,这是我们所不能接受的,我们的唯一解决方案就是让where条件和排序字段都用上索引

 

解决办法就是建立联合索引:

alter table acct_trans_log add index idx_acct_id_create_time(acct_id,create_time)

然后执行sql:

select * from acct_trans_log WHERE  acct_id = 3095  order by create_time desc limit 0,10

耗时: 0.016s

 联合索引让where条件字段和排序字段都用上了索引,问题解决了!

 

联合索引使用的原理

但是为什么能解决这个问题呢,这时大家可能就会记住一个死理,就是联合索引可以解决where过滤和排序的问题,也不去了解

其原理,这样是不对的,因为当情况发生变化,就懵逼了,下面我们再看一个sql:

select * from acct_trans_log force index(idx_acct_id_create_time) WHERE  acct_id in(3095,1000000000009000757)  order by create_time desc limit 0,10

耗时:1.391s

索引还是用idx_acct_id_create_time,时间居然慢下来了

执行计划是:

 

 

  看执行计划,排序用到了filesort,也就是说,排序未用到索引。

 

那么我们还是来看看,索引排序的原理,我们先来看一个sql:

select * from acct_trans_log ORDER BY create_time limit 0,100

耗时:0.029s

执行计划为:

 这里执行的步骤是,先从索引树中,按时间升序取出前100条,因为索引是排好序的,直接左序遍历即可了

因此,这里mysql并没有做排序动作,如果想降序,则右序遍历索引树,取出100条即可,查询固然快,

 

那么联合索引的时候,是怎样的呢?

select * from acct_trans_log WHERE  acct_id = 3095  order by create_time desc limit 0,10

使用组合索引:idx_acct_id_create_time

这个时候,因为acct_id是联合索引的前缀,因此可以很快实行检索,

如果sql是

select * from acct_trans_log WHERE  acct_id = 3095

出来的数据是按如下逻辑排序的

3095+time1

3095+time2

3095+time3

默认是升序的,也就是说,次sql相当于

select * from acct_trans_log WHERE  acct_id = 3095 order by create_time

他们是等效的。

如果我们把条件换成order by create_time desc limit 0,10呢

这时候,应该从idx_acct_id_create_time树右边叶子节点倒序遍历,取出前10条即可

因为数据的前缀都是3095,后缀是时间升序。那么我们倒序遍历出的数据,刚好满足order by create_time desc

因此也无需排序。

 

那么语句:

select * from acct_trans_log force index(idx_acct_id_create_time) WHERE  acct_id in(3095,1000000000009000757)  order by create_time desc limit 0,10

为什么排序无法用索引呢?

我们先分析下索引的排序规则

已知:id1<id2<id3...  time1<time2<time3....

查询结果集排序如下:

id1+time1

id1+time2

id1+time3

id2+time1

id2+time2

id2+time3

 

索引出来的默认排序是这样的,id是有序的,时间是无序的,因为有2个id,优先按id排序,时间就是乱的了,

这样排序将会用filesort,这就是慢的原因,也是排序没有用到索引的原因。

  

查询计划使用以及使用说明

table:显示这一行数据是关于哪张表的

type:显示使用了何种类型,从最好到最差的连接类型为const,eq_ref,ref,range,index,all

possible_keys:显示可能应用在这张表中的索引。如果为空,没有可能的索引

key:实际使用的索引,如果为null,则没有使用索引。

key_len:使用的索引的长度。在不损失精确性的情况下,长度越短越好

ref:显示索引的哪一列被使用了,如果可能的话,是一个常数

rows:mysql认为必须检查的用来返回请求数据的行数

  • 2019-12-05 17:01:36

    Vue 结合 Axios 接口超时统一处理

    当网路慢的时候。又或者公司服务器不在内地的时候,接口数据请求不回来超时报错的情况相信大家肯定遇到过的,这里我把我公司项目请求超时的处理方法分享下,希望看过后有帮助。

  • 2019-12-05 17:13:40

    JS模板工具lodash.template的简单用法

    lodash是从underscore分支的一个项目,之前我写了一篇JS模板工具underscore.template的简单用法,lodash跟underscore很相似,这也简单介绍一下lodash的template方法。 先把underscore的文章中用过的代码贴过来,把underscore的js文件换成lodash的js,其他一字不改,然后我们试试:

  • 2019-12-06 10:47:29

    date-fns日期工具的使用方法详解

    isToday() 判断传入日期是否为今天 isYesterday() 判断传入日期是否为昨天 isTomorrow() 判断传入日期是否为 format() 日期格式化 addDays() 获得当前日期之后的日期 addHours() 获得当前时间n小时之后的时间点 addMinutes() 获得当前时间n分钟之后的时间 addMonths() 获得当前月之后n个月的月份 subDays() 获得当前时间之前n天的时间 subHours() 获得当前时间之前n小时的时间 subMinutes() 获得当前时间之前n分钟的时间 subMonths() 获得当前时间之前n个月的时间 differenceInYears() 获得两个时间相差的年份 differenceInWeeks() 获得两个时间相差的周数 differenceInDays() 获得两个时间相差的天数 differenceInHours() 获得两个时间相差的小时数 differenceInMinutes() 获得两个时间相差的分钟数

  • 2019-12-06 10:49:39

    npm 查看源 换源

    npm,cnpm,查看源,切换源,npm config set registry https://registry.npmjs.org

  • 2019-12-06 11:01:31

    npm发布包流程详解 有demo

    npm发布包步骤,以及踩过的坑(见红颜色标准): 1.注册npm账号,并完成Email认证(否则最后一步提交会报Email错误) 2.npm添加用户或登陆:npm adduser 或 npm login

  • 2019-12-06 13:16:18

    vue mixins组件复用的几种方式

    最近在做项目的时候,研究了mixins,此功能有妙处。用的时候有这样一个场景,页面的风格不同,但是执行的方法,和需要的数据非常的相似。我们是否要写两种组件呢?还是保留一个并且然后另个一并兼容另一个呢? 不管以上那种方式都不是很合理,因为组件写成2个,不仅麻烦而且维护麻烦;第二种虽然做了兼容但是页面逻辑造成混乱,必然不清晰;有没有好的方法,有那就是用vue的混合插件mixins。混合在Vue是为了提出相似的数据和功能,使代码易懂,简单、清晰。

  • 2019-12-06 13:26:30

    vue的mixins混入合并规则

    混入minxins:分发vue组件中可复用功能的灵活方式。混入对象可以包含任意组件选项。组件使用混入对象时,所有混入对象的选项将混入该组件本身的选项。

  • 2019-12-06 16:50:34

    Intellij idea 如何关闭无用的提示

    Linux:Settings —> Editor —> Inspections —> General —> Duplicated Code Mac:Preferences --> Editor —> Inspections —> General —> Duplicated Code fragment 将对应的勾去掉。