前言
又和你们见面了!又两周过去了,个人云笔记里又多了几篇写了一半的文章草稿。有的是由于质量没有达到预期还准备再加点内容,有的则彻底是一个灵感而已,内容彻底木有。羡慕不少大佬们,一周能产出五六篇文章,给我两个肝我都不够。好了,很少说废话了...php
最近在线上环境遇到了一次SQL慢查询引起的数据库故障,影响线上业务。通过排查后,肯定缘由是「SQL在执行时,MySQL优化器选择了错误的索引(不该该说是“错误”,而是选择了实际执行耗时更长的索引)」。在排查过程当中,查阅了许多资料,也学习了下MySQL优化器选择索引的基本准则,在本文中进行解决问题思路的分享。本人MySQL了解深度有限,若是错误欢迎理性讨论和指正。html
「在此次事故中也能充分看出深刻了解MySQL运行原理的重要性,这是遇到问题时可否独立解决问题的关键。」 试想一个月黑风高的夜晚,公司线上忽然挂了,而你的同事们都不在线,就你一我的有条件解决问题,这时候若是被工程师的基本功把你卡住了,就问你尴不尴尬...mysql
「本文的主要内容:」
- 故障描述
- 问题缘由排查
- MySQL索引选择原理
- 解决方案
- 思考与总结
❝ 请你们多多支持个人原创技术公众号:后端技术漫谈 ❞
正文
故障描述
在7月24日11点线上某数据库忽然收到大量告警,慢查询数超标,而且引起了链接数暴增,致使数据库响应缓慢,影响业务。看图表慢查询在高峰达到了每分钟14w次,在平时正常状况下慢查询数仅在两位数如下,以下图:
面试
赶忙查看慢SQL记录,发现都是同一类语句致使的慢查询(隐私数据例如表名,我已经隐去):算法
select * from sample_table where 1 = 1 and (city_id = 565) and (type = 13) order by id desc limit 0, 1
看起来语句很简单,没什么特别的。可是每一个执行的查询时间达到了惊人的44s。
sql
简直耸人听闻,这已经不是“慢”能形容的了...数据库
接下来查看表数据信息,以下图:
segmentfault
能够看到表数据量较大,预估行数在83683240,也就是8000w左右,「千万数据量的表」。后端
大体状况就是这样,下面进入排查问题的环节。设计模式
问题缘由排查
首先固然要怀疑会不会该语句没走索引,查看建表DML中的索引:
KEY `idx_1` (`city_id`,`type`,`rank`), KEY `idx_log_dt_city_id_rank` (`log_dt`,`city_id`,`rank`), KEY `idx_city_id_type` (`city_id`,`type`)
请忽略idx_1和idx_city_id_type两个索引的重复,这都是历史遗留问题了。
「能够看到是有idx_city_id_type和idx_1索引的」,咱们的查询条件是city_id和type,这两个索引都是能走到的。
可是,咱们的查询条件真的只要考虑city_id和type吗?(机智的小伙伴应该注意到问题所在了,先往下讲,留给你们思考)
既然有索引,接下来就该看该语句实际有没有走到索引了,MySQL提供了Explain能够分析SQL语句。Explain 用来分析 SELECT 查询语句。
Explain比较重要的字段有:
- select_type : 查询类型,有简单查询、联合查询、子查询等
- key : 使用的索引
- rows : 预计须要扫描的行数
更多详细Explain介绍能够参考:MySQL 性能优化神器 Explain 使用分析
咱们使用Explain分析该语句:
select * from sample_table where city_id = 565 and type = 13 order by id desc limit 0,1
获得结果:
能够看出,虽然possiblekey有咱们的索引,可是最后走了主键索引。而表是千万级别,「而且该查询条件最后实际是返回的空数据」,也就是MySQL在主键索引上实际检索时间很长,致使了慢查询。
咱们可使用force index(idx_city_id_type)让该语句选择咱们设置的联合索引:
select * from sample_table force index(idx_city_id_type) where ( ( (1 = 1) and (city_id = 565) ) and (type = 13) ) order by id desc limit 0, 1
此次明显执行的飞快,分析语句:
实际执行时间0.00175714s,走了联合索引后,再也不是慢查询了。
问题找到了,总结下来就是:「MySQL优化器认为在limit 1的状况下,走主键索引可以更快的找到那一条数据,而且若是走联合索引须要扫描索引后进行排序,而主键索引天生有序,因此优化器综合考虑,走了主键索引。实际上,MySQL遍历了8000w条数据也没找到那个天选之人(符合条件的数据),因此浪费了不少时间。」
MySQL索引选择原理
优化器索引选择的准则
MySQL一条语句的执行流程大体以下图,而「查询优化器」则是选择索引的地方:
引用参考文献一段解释:
❝ 首先要知道,选择索引是MySQL优化器的工做。 而优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。在数据库里面,扫描行数是影响执行代价的因素之一。扫描的行数越少,意味着访问磁盘数据的次数越少,消耗的CPU资源越少。 「固然,扫描行数并非惟一的判断标准,优化器还会结合是否使用临时表、是否排序等因素进行综合判断。」
❞
总结下来,优化器选择有许多考虑的因素:「扫描行数、是否使用临时表、是否排序等等」
咱们回头看刚才的两个explain截图:
走了「主键索引」的查询语句,rows预估行数1833,而强制走「联合索引」行数是45640,而且Extra信息中,显示须要Using filesort进行额外的排序。因此在不增强制索引的状况下,「优化器选择了主键索引,由于它以为主键索引扫描行数少,并且不须要额外的排序操做,主键索引天生有序。」
rows是怎么预估出来的
同窗们就要问了,为何rows只有1833,明明实际扫描了整个主键索引啊,行数远远不止几千行。实际上explain的rows是MySQL「预估」的行数,「是根据查询条件、索引和limit综合考虑出来的预估行数。」
MySQL是怎样获得索引的基数的呢?这里,我给你简单介绍一下MySQL采样统计的方法。 为何要采样统计呢?由于把整张表取出来一行行统计,虽然能够获得精确的结果,可是代价过高了,因此只能选择“采样统计”。 采样统计的时候,InnoDB默认会选择N个数据页,统计这些页面上的不一样值,获得一个平均值,而后乘以这个索引的页面数,就获得了这个索引的基数。 而数据表是会持续更新的,索引统计信息也不会固定不变。因此,当变动的数据行数超过1/M的时候,会自动触发从新作一次索引统计。 在MySQL中,有两种存储索引统计的方式,能够经过设置参数innodb_stats_persistent的值来选择: 设置为on的时候,表示统计信息会持久化存储。这时,默认的N是20,M是10。 设置为off的时候,表示统计信息只存储在内存中。这时,默认的N是8,M是16。 因为是采样统计,因此无论N是20仍是8,这个基数都是很容易不许的。
咱们可使用analyze table t命令,能够用来从新统计索引信息。可是这条命令生产环境须要联系DBA,因此我就不作实验了,你们能够自行实验。
索引要考虑 order by 的字段
为何这么说?由于若是我这个表中的索引是city_id,type和id的联合索引,那优化器就会走这个联合索引,由于索引已经作好了排序。
更改limit大小能解决问题?
把limit数量调大会影响预估行数rows,进而影响优化器索引的选择吗?
答案是会。
咱们执行limit 10
select * from sample_table where city_id = 565 and type = 13 order by id desc limit 0,10
图中rows变为了18211,增加了10倍。若是使用limit 100,会发生什么?
优化器选择了联合索引。初步估计是rows还会翻倍,因此优化器放弃了主键索引。宁愿用联合索引后排序,也不肯意用主键索引了。
为什么忽然出现异常慢查询
问:这个查询语句已经在线上稳定运行了很是长的时间,为什么此次忽然出现了慢查询?
答:之前的语句查询条件返回结果都不为空,limit1很快就能找到那条数据,返回结果。而此次代码中查询条件实际结果为空,致使了扫描了所有的主键索引。
解决方案
知道了MySQL为什么选择这个索引的缘由后,咱们就能够根据上面的思路来列举出解决办法了。
主要有两个大方向:
- 强制指定索引
- 干涉优化器选择
强制选择索引:force index
就像上面我最开始的操做那样,咱们直接使用force index,让语句走咱们想要走的索引。
select * from sample_table force index(idx_city_id_type) where ( ( (1 = 1) and (city_id = 565) ) and (type = 13) ) order by id desc limit 0, 1
这样作的优势是见效快,问题立刻就能解决。
缺点也很明显:
- 高耦合,这种语句写在代码里,会变得难以维护,若是索引名变化了,或者没有这个索引了,代码就要反复修改。属于硬编码。
- 不少代码用框架封装了SQL,force index()并不容易加进去。
「咱们换一种办法,咱们去引导优化器选择联合索引。」
干涉优化器选择:增大limit
经过增大limit,咱们可让预估扫描行数快速增长,好比改为下面的limit 0, 1000
SELECT * FROM sample_table where city_id = 565 and type = 13 order by id desc LIMIT 0,1000
这样就会走上联合索引,而后排序,可是这样强行增加limit,其实总有种面向黑盒调参的感受。咱们还有更优美的解决方案吗?
干涉优化器选择:增长包含order by id字段的联合索引
咱们这句慢查询使用的是order by id,可是咱们却没有在联合索引中加入id字段,致使了优化器认为联合索引后还要排序,干脆就不太想走这个联合索引了。
咱们能够新建city_id,type和id的联合索引,来解决这个问题。
这样也有必定的弊端,好比我这个表到了8000w数据,创建索引很是耗时,并且一般索引就有3.4个g,若是无限制的用索引解决问题,可能会带来新的问题。表中的索引不宜过多。
干涉优化器选择:写成子查询
还有什么办法?咱们能够用子查询,在子查询里先走city_id和type的联合索引,获得结果集后在limit1选出第一条。
可是子查询使用有风险,一版DBA也不建议使用子查询,会建议你们在代码逻辑中完成复杂的查询。固然咱们这句并不复杂啦~
Select * From sample_table Where id in (Select id From `newhome_db`.`af_hot_price_region` where (city_id = 565 and type = 13)) limit 0, 1
还有不少解决办法...
SQL优化是个很大的工程,咱们还有很是多的办法可以解决这句慢查询问题,这里就不一一展开了。留给你们作为思考题了。
总结
本文带你们回顾了一次MySQL优化器选错索引致使的线上慢查询事故,能够看出MySQL优化器对于索引的选择并不仅仅依靠某一个标准,而是一个综合选择的结果。我本身也对这方面了解不深刻,还须要多多学习,争取可以好好的作一个索引选择的总结(挖坑)。不说了,拿起巨厚的《高性能MySQL》,开始...
压住个人泡面...
「最后作个文章总结:」
- 该慢查询语句中使用order by id致使优化器在主键索引和city_id和type的联合索引中有所取舍,最终致使选择了更慢的索引。
- 能够经过强制指定索引,创建包含id的联合索引,增大limit等方式解决问题。
- 平时开发时,尤为是对于特大数据量的表,要注意SQL语句的规范和索引的创建,避免事故的发生。
往期推荐
交流群 | 个人“惟一指定”技术交流群创建了
SQL调优 | SQL 书写规范及优化技巧(下)
开源实战 | Canal生产环境常见问题总结与分析
系统设计 | 经过Binlog来实现系统间数据同步
MySQL | 敖丙的数据库调优最佳实践
参考
《高性能MySQL》
MySQL优化器 limit影响的case:
https://www.cnblogs.com/xpchild/p/3878417.html
mysql中走与不走索引的状况聚集(待全量实验):
https://www.cnblogs.com/gxyandwmm/p/13363100.html
「MySQL ORDER BY主键id加LIMIT限制走错索引:」
https://www.jianshu.com/p/caf5818eca81
【业务学习】关于MySQL order by limit 走错索引的探讨:
http://www.javashuo.com/article/p-wlkduonm-cm.html
MySQL为何有时候会选错索引?:
http://www.javashuo.com/article/p-shonegzz-hq.html
关注我
我是一名后端开发工程师。主要关注后端开发,数据安全,爬虫,物联网,边缘计算等方向,欢迎交流。
各大平台均可以找到我
- 「微信公众号:后端技术漫谈」
- 「Github:@qqxx6661」
- CSDN:@蛮三刀把刀
- 知乎:@后端技术漫谈
- 简书:@蛮三刀把刀
- 掘金:@蛮三刀把刀
- 腾讯云+社区:@后端技术漫谈
原创文章主要内容
- 后端开发
- Java面试
- 设计模式/数据结构/算法题解
- 爬虫/边缘计算/物联网
- 读书笔记/逸闻趣事/程序人生
我的公众号:后端技术漫谈
我的公众号:后端技术漫谈「若是文章对你有帮助,不妨收藏,转发,在看起来~」