最近做压测的时候,发现有个接口的压测,线程数才20,而其他接口的都相对而言比较大。当时第一反应可能是自己压测错了,试了好几次,发现压测的结果都差不多就是20.当时没没有深究原因。
后来老大说有时间让我看看原因。然后我看了下这个接口,压根没什么东西,就2条sql。用这两条sql去查询了一下,发现查询时间过长。然后我把这2条sql贴到navicat里面看了下,以下是我整个过程的分析:
原始的sql是这两条:
SELECT * FROM SMS_TEMPLATE_N ORDER BY TEMPLATE_TYPE=0 ASC, TEMPLATE_TYPE,TEMPLATE_ID;
SELECT SMS_TEMPLATE_ID,COUNT(*) FROM SMS_HISTORY_N WHERE SMS_STATUS = 1 AND SEND_TIME BETWEEN '2019-05-15 00:00:00' AND '2019-05-16 10:55:00' GROUP BY SMS_TEMPLATE_ID;
先一条一条的分析:
第一条:他的目的是查询搜有的短信模板然后按模板类型升序排列,但是类型为0时排在最后面。
当时感觉这条sql没什么可以优化的了。但是程序打印的时候很长。请岳晨洋帮我看了下,他问我为什么要这样写,为什么要排序,然后看了下cms_boss的页面这个版块,和跳转到这个页面的部分,发现其实排不排序最后效果都能实现,然后完全可以把关于排序的去掉,所以sql改成了这样子:
SELECT * FROM SMS_TEMPLATE_N ORDER BY TEMPLATE_ID;
然后去模拟环境执行这条sql,执行时间只有1毫秒,我感觉确实快了很多。
后来想了想,不对呀,我为什么会这样写呢?我对比了有参数查询和没有参数查询的sql,在看了cms_boss的部分,确实是我排序的部分有重复的部分。然后我又想了想,不对呀,这2条sql的执行计划是一模一样的,不应该有这么大的差别呀,我把原始的sql贴到模拟环境执行sql,看了下时间也是1毫秒,所以说,这条sql,除了有业务的重合,本身不是造成执行时间很久的原因。
然后看第二条sql:
SELECT SMS_TEMPLATE_ID,COUNT(*) FROM SMS_HISTORY_N WHERE SMS_STATUS = 1 AND SEND_TIME BETWEEN '2019-05-15 00:00:00' AND '2019-05-16 10:55:00' GROUP BY SMS_TEMPLATE_ID;
这个是执行时间,然后在看一下执行计划,
看到这个的时候,我当时的感觉就是已经用到索引了呀,怎么还是这么慢呢。
,百度了下type:
type,访问类型,sql查询优化中一个很重要的指标,结果值从好到坏依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
一般来说,好的sql查询至少达到range级别,最好能达到ref
先备注下这些指标分别是什么意思:
system :表只有一行记录(等于系统表),这是const类型的特例,平时不会出现,可以忽略不计
const:表示通过索引一次就找到了,const用于比较primary key 或者 unique索引。因为只需匹配一行数据,所有很快。
eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键 或 唯一索引扫描。
ref:非唯一性索引扫描,返回匹配某个单独值的所有行。本质是也是一种索引访问,它返回所有匹配某个单独值的行,然而他可能会找到多个符合条件的行,所以它应该属于查找和扫描的混合体
fulltext:全文搜索
ref_or_null:与ref类似,但包括NULL
index_merge:表示出现了索引合并优化(包括交集,并集以及交集之间的并集),但不包括跨表和全文索引。 这个比较复杂,目前的理解是合并单表的范围索引扫描(如果成本估算比普通的range要更优的话)
unique_subquery:在in子查询中,就是value in (select...)把形如“select unique_key_column”的子查询替换。PS:所以不一定in子句中使用子查询就是低效的!
index_subquery:同上,但把形如”select non_unique_key_column“的子查询替换
range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了那个索引。一般就是在where语句中出现了bettween、<、>、in等的查询。这种索引列上的范围扫描比全索引扫描要好。只需要开始于某个点,结束于另一个点,不用扫描全部索引
index:Full Index Scan,index与ALL区别为index类型只遍历索引树。(Index与ALL虽然都是读全表,但index是从索引中读取,而ALL是从硬盘读取)
all:Full Table Scan,遍历全表以找到匹配的行
我又看了下公司的规范,
看完这2条,我突然意识到,看执行计划,我好像少看了一个东西,extra,然后我找了一下关于extra的取值的意义:
Using filesort : mysql对数据使用一个外部的索引排序,而不是按照表内的索引进行排序读取。也就是说mysql无法利用索引完成的排序操作成为“文件排序”
Using temporary: 使用临时表保存中间结果,也就是说mysql在对查询结果排序时使用了临时表,常见于order by 和 group by
Using index: 表示相应的select操作中使用了覆盖索引(Covering Index),避免了访问表的数据行,效率高 。如果同时出现Using where,表明索引被用来执行索引键值的查找; 如果没用同时出现Using where,表明索引用来读取数据而非执行查找动作
覆盖索引(Covering Index):也叫索引覆盖。就是select列表中的字段,只用从索引中就能获取,不必根据索引再次读取数据文件,换句话说查询列要被所建的索引覆盖。
注意: a、如需使用覆盖索引,select列表中的字段只取出需要的列,不要使用select *
b、如果将所有字段都建索引会导致索引文件过大,反而降低crud性能
Using where : 使用了where过滤
Using join buffer : 使用了链接缓存
Impossible WHERE: where子句的值总是false,不能用来获取任何元祖
select tables optimized away: 在没有group by子句的情况下,基于索引优化MIN/MAX操作或者对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段在进行计算,查询执行计划生成的阶段即可完成优化
distinct: 优化distinct操作,在找到第一个匹配的元祖后即停止找同样值得动作
看完这个我还是没觉得这个有什么问题存在的,然后我有重新看了下表结构,有一个联合索引在的,仔细瞅了2眼,感觉那个联合索引有点怪,
我尝试去掉了SMS_TEMPLATE_ID,然后重新执行了这条sql:
看到效果没,立竿见影,快了不止一点点,然后我重新执行了下执行计划:
然后回到刚刚看的type的解释,上面又说到当范围查找比如between的时候,一般type的类型就应该是range,我是在改出来效果之后才想到这一点,真的是看东西只看了却没有好好吸收。这是很应该反思的一点。
这个语句用到了group by,而用到它的时候,一般extra会有Using temporary,改完之后的执行计划,正好又验证了这个结论。
效果虽然出来了,但是我还是不知道为什么是这个结论,又瞅了眼这个sql和表结构,我试着把联合索引加上SMS_TEMPLATE_ID,不过把他放在最后面,然后重新执行这个sql,然后看了下执行时间:
然后看了下执行计划:
这个结果就已经很明朗了,导致最开始现象的原因是索引建立的不合适,我的sql执行时,不符合最左前缀原则,所以才会导致那种效果,既然加上SMS_TEMPLATE_ID放在联合索引的最后面和不加它效果是一样的,那索性就不加它了(后来证明这个想法是错的)。
结果出来了之后,我们再来回顾下,我看公司规范的时候,截的那个图,order by的有序性,这一点同样是适用于group by的,group by的字段是组合索引的一部分,应该放在索引组合顺序的最后面。
那现在加不加SMS_TEMPLATE_ID,好像执行计划,执行速度都看不出来什么区别,那么是不是就不加了?以我的个性来说,我是不会加的,后来把sql给到公司dba看,他给我的建议是这个索引是需要在最后面加上SMS_TEMPLATE_ID这个字段的,最直接的验证效果就是拉大时间间距,多查几天的,我试了下,确实多查几天效果就变化很明显,所以这个是应该加上的。也就是说有联合索引且有group by的时候,联合索引的最后面是要加上group by这个字段的。
这个结论出来之后,我把这个例子的答案跟朋友分享了下,他跟我说了一句:索引没用好会有回表现象。
听到这句话,第一反应眉头紧锁,额,我好像对回表现象没有任何印象。百度了下回表现象:
朋友的解释:通过二级索引查到主键索引,再通过主键索引查全表,时间复杂度到N方了
我截了一个定义,两个解释,重点看一下第二点:使用了索引,Extra中是using index & using where ,这个情况下会回表查询数据。
再看一眼一开始没改的sql,它的执行计划:
这不正是说明出现了回表查询现象。这一点正好跟截图的开发规范的第六条相呼应。
这个分析过程,我感觉最深的就是多思考,很多东西,看了很多遍,但是它并不是我的,我也没有良好的吸收,每次都是需要的时候看看,这个明显拉长了找问题的时间。
改完这个之后,我重新做了这个接口的压测,线程数变成了180个:
top:
pinpoint:
memory:
tps:
cpu:
response time: