千万级数据表选错索引致使的线上慢查询事故

千万级数据表选错索引致使的线上慢查询事故

前言

又和你们见面了!又两周过去了,个人云笔记里又多了几篇写了一半的文章草稿。有的是由于质量没有达到预期还准备再加点内容,有的则彻底是一个灵感而已,内容彻底木有。羡慕不少大佬们,一周能产出五六篇文章,给我两个肝我都不够。好了,很少说废话了...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为什么选择这个索引的缘由后,咱们就能够根据上面的思路来列举出解决办法了。

主要有两个大方向:

  1. 强制指定索引
  2. 干涉优化器选择

强制选择索引: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面试
  • 设计模式/数据结构/算法题解
  • 爬虫/边缘计算/物联网
  • 读书笔记/逸闻趣事/程序人生

    我的公众号:后端技术漫谈

    千万级数据表选错索引致使的线上慢查询事故我的公众号:后端技术漫谈「若是文章对你有帮助,不妨收藏,转发,在看起来~」

相关文章
相关标签/搜索