# MySQL 慢 SQL 处理心得

最近接手了一个数据量非常大的项目,表数据量都在千万级别,在接手的这 3 个月内遇到多次慢 SQL 导致数据库 CPU 上升,甚至影响到了其他服务,其中有一直遗留的,也有最近才冒出来的,也有极少新功能上线出现的。我也是一步一步的将它们处理直到没有一个慢 SQL

# 前言

因为生产环境,公司性质,项目原因,人员安排等,这次处理慢 SQL 将会更加贴近 “生活”,并不是一次纯粹的优化而是处理,所有会以一个并不太专业的方式讲给大家听

# 遇到的场景分享记录

先说点其它的:发现慢 SQL 一般是通过告警提示,大量慢 SQL 会导致服务器运行 CPU 上升,紧急情况下应该第一时间重启服务,或者回退版本,确保数据库正常,不会对其它服务造成二次影响,再向下思考解决思路

# 1. 的确没有索引,要查的 sql 在当前表中确实没有一个能用的索引

  • 紧急且影响很大的话可以先重启服务,确保先确保数据库 CPU 正常,再新增对应的索引或者急修复:如暂停此功能使用等
  • 有时间了先看代码此处业务逻辑,检查是否可以替换或者优化使它能走已有的索引
  • 如果有可以更改那就更改,如果不行就只能加个索引了,加索引也要看能否和其它的一起成为一个联合索引,尽量减少索引数量
  • 如果要加索引,需要考虑当前表使用频率,以免短时间加不上

# 2. 走了索引,索引字段全走了但依旧很慢

  1. 分页查询用 limit 做分页,导致在几十万页时查询太慢。
1.1 如果是前端页面展示,则页面默认打开不查询,必须填写指定参数,减少分页数
    1.2 如果是后端定时调度,则改为根据唯一约束索引进行大于分页查询。
       1.2.1 最优是取联合索引中字段,这个字段必须能保证这条记录唯一且是联合索引最后个值(最优解)
       1.2.2 根据 id 字段进行分页(推荐)(但如果扫描的数据占比高的话,可以能会导致 MySql 走主键索引(最后可能变成原有聚合索引字段又加一个 id )
       原因:不管你用哪个字段做大于分页,你都需要保证这个字段在这条 sql 中是有序的(一般加 order by),与唯一性,不然会导致分页数据量错乱
  1. 索引字段全走了,依旧未覆盖这条 sql 的全部条件
2.1 有些 sql 会有 group by,order by,DISTINCT 这种条件在里面,会导致在内存中做数据去重排序
    2.1.1 group by,DISTINCT会在内存中做去重,其中group by要对所有的去重,DISTINCT 在大量重复数据下也会有影响,建议字段放在联合索引第一位
    2.1.2.order by,是对数据进行排序,一般配合分页使用,所以在条件过滤后在索引树上有排序最好,建议放在聚合索引最后面
    	 2.1.2.1 注意有些 mysql 版本是不能在联合索引中隔着一个去找的,所以前面的字段最好要匹配全
2.2 还有一些 sql 会有 SUM 等计算函数,但索引树上没有这些字段导致大量 回表(根据主键 ID 去查)操作取数据,这时最好将这些字段冗余到联合索引的最后一位
  1. sql 已经优化极限了,那就只能从业务角度, 分片操作,可以根据创建时间几个月的计算,也可以根据 id 分页固定取多少条记录数 (可能会导致前面排序问题)

# 3. 走的索引有问题

  1. 索引走对了但看 key_len 时发现字段没有走全导致很慢
1.1 有因为联合索引字段中间的字段有缺失,导致后面的条件字段没走法走这个索引
    1.1.1 先看代码能否加这个字段,如果不行,那就考虑能不能调整聚合索引的字段顺序,还不行只能调整业务代码拆分 sql 了
1.2.有因为不走的那个字段用的是 in,in 里面是无序,如果in的值数量和要扫描的数满足一定条件时(这个条件不清楚,猜应该与 in 的这个字段长度有一定关系),会在索引树已满足前面条件的位置对后面的进行全量扫描匹配
    1.2.2 万能解决是循环取值,可以对 in 的个数做下简单的测试走索引,然后分片执行
  1. 索引没有走对,看 explain 后 key 字段的值来确定索引是否正确,走不对索引执行速度自然有影响
2.1 最常见的是根据 id 进行排序,通过 id 进行分页,可能会导致 mysql 判断这条 sql 走主键索引更合适
    2.1.1 最有效解决办法是在原有索引最后再加一个 id 字段(需要考虑表数据量大且服务正在使用时能否快速加上去)(最好先把要操作这个表的功能停个几分钟)
    2.1.2 换联合索引中的字段,要么具有唯一性,要么像时间一样可定个范围(查的时候需要考虑有数据,但正好你查的这个范围没数据,需要count下)
2.2 还有一种有一个单字段的非唯一索引和一个包含了这个单字段的联合索引,如果遇到了 in 问题,就会有可能走错索引
   (原因:因为b+树的特性,字段越少,单个块存储的记录数越多,在不走第二个索引的预估下,第一个索引更快)
   2.2.1 这种一个字段的索引一般可以删掉,用也包含它的联合索引就好,一般情况在索引的其它问题上可以忽略单个它能带来的提升(联合索引的第一个字段是它最好)
   2.2.2 之后就是前面的 in 问题了
  1. 索引设计不合理,导致 mysql 预估有误走错索引,都需要根据业务场景以及使用到的地方分析是否调整索引
3.1 一个不是很合理的解决办法是通过 FORCE INDEX 强制指定哪个索引
3.2 大量单字段索引
    3.2.1.有些索引是因为表设计之初考虑不周新增的单个字段索引,如果创建时间,修改时间,这些需要仔细的分析后,再决定是否移除或者被其它索引兼容
    3.2.2.有些在代码中不使用到此索引的,可以直接删除(需要考虑到更新,删除语句条件,需要仔细的分析)
3.3 有些多字段联合索引可以包含另一个索引
    3.3.1 如果单个索引的字段在多个的索引的第一个,则可以直接删掉单个索引
    3.3.2 如果单个索引的字段在多个的索引非第一个,则看代码能否调整多字段索引的顺序或为使用到单字段的索引补充条件实现走一个索引,。然后删除单个索引的
3.4 有些索引仅仅因为字段顺序不同,就变成两个索引了,更改代码,调整另一边的条件顺序,移除不用的索引

# 浅谈两个有意思的慢 SQL

# 第一个

这个慢 SQL 最开始是因为 RDS (阿里云 MYSQL) CPU 告警,经过排除确定 SQL 触发来源是有人在前端页面根据 ID 分页查询,SQl 没有走正确的索引,联合索引本身建立的有一定缺陷,SQL 查询条件相对这个正确的联合索引有一定的缺失。
假设:联合索引是 (a,b,c,d)
缺陷原因:SQL 在 c 的时候用的是 in,b 并不是一个区分度很高的字段,d 是
走错的索引是:主键索引 (id),因为用 ID 分页
印象深刻的原因是:只有前面页面查询条件包含在某个范围的时候会触发 (数据量吧),本地无法复现,只能线上 explain 看到两个条件下使用了不同索引
最后解决办法:调整了联合索引字段顺序

# 第二个

是因为联合索引有 a,b 两个,第二个是 in , 经过 explain 的 key_len 字段发现只走了 a 索引,b 没有走,后面调整了 in 的个数就好了

# 总结

慢 SQL 处理在实际的工作中考虑总是多方面的,其实最后有部分慢 SQL 优化到极限是 1 秒多一些,再继续优化收获不会很高反而其他损耗更高,最后直接动态数据原改为查数仓了,从业务角度是可以的,所以处理慢 SQL 这不是纯粹的技术方面并不是站在要优化的角度的吧,要考虑实际情况,总之不要慌先把问题小化,再解决问题