DBA在群里报产线DB有故障,数据库所在机器的CPU使用率高达95%, 短时间内报了500+条慢查询,其中出现最多的是下面一条SQL语句:
产线出故障时,运维同学一般都是描述现象,一个现象要转换为可以具体排查的问题,往往需要做一些信息的确认和提取工作的。
就这个问题来说,CPU占用率高是现象,DBA帮我们做了第一步的信息提取:发现一条慢SQL。
但是这个问题还不够具体,比如:究竟是不是这条慢SQL导致了CPU高是需要确认的,另外这个慢SQL究竟有多慢也不知道,所以研发还需要做进一步的信息提取。
在我们的追问下,DBA又给出数据库系统记录的慢查询具体信息:
从截图上可以看出,这条SQL扫描了24万行记录,最终返回的结果只用到了256行,执行时间1~10s不等。
SQL中访问的唯一一张表webcal_entry_user, 是日程业务中的参会人信息表,表中有2000多万条数据,表和索引结构如下:
可以说,这条SQL结构并不复杂,没有联表,没有排序,唯一需要看的是索引的命中情况和是否命中最优索引,通过explain来分析下(下图是从beta截出来的,除了扫描行数与产线不一样,其它都相同):
从分析的结果来看只有一个条件过滤cal_login in (xxx)命中的了索引,这个字段的区分度并不差,可惜的是要查300用户,并且数年积累下来共计有24万条参会记录。
换句话说,用索引筛选完后,还有24万条记录需要从DB加载到内存用where条件再来筛一遍,扫描行数太多是慢的根因。
先说结论,最终给DBA发了一条索引变更语句,产线变更后,扫描行数也下降到了1000行左右,常规执行时间下降到了34ms。
ALTER TABLE `webcal_entry_user` DROP INDEX `idx_cal_login`, ADD INDEX `idx_cal_login` (`cal_login`,`end_time`);
对于产线故障来说,最终的解法往往都比较简单,复杂的地方在思考方案过程中所运用的知识和原理。对于联合索引来说,可能涉及到的知识点例如:
索引的目的是为了快速过滤出业务需要的最小数据集合,如果任何一个单列索引过滤效果都不明显,就会考虑是不是再加一个过滤条件让最终结果的数据小一些。
**知识点:**用索引筛掉的数据越多,需要从磁盘加载到内存的数据集就越小。不过索引本身也是有开销的,所以创建索引时需要取一个权衡:能够解决问题的最小索引。
就以这个问题为例,cal_login字段过滤完后,还有24万条数据,就必须再找一个筛选效率好的条件来作为第2个索引字段,分析过程是:
为什么最终我们添加end_time字段作为索引而不用start_time呢?
基于对业务的理解来判断:
所以做技术的人,理解业务很重要。
那为什么我们是建立(cal_login
,end_time
)的联合索引,而不是(end_time
,cal_login
)的联合索引呢?
**知识点:**多个字段建立的索引是有顺序的,只有左边的字段命中了索引,右边的字段才可能命中索引。
假如我们对(a, b)字段建立了索引,
Case1: 当where条件为下面的两种时
a = 1
a = 1 and b = 2
都是可以命中索引的。
Case2: 当where条件为
b= 2 and a =1
也是可以命中索引的。不是我们写的对,而是Mysql优化器帮我们做了调整,调整后按 a = 1 and b = 2
来执行去匹配索引中的字段顺序(a, b)。
Case3: 如果执行 b = 2
就命中不了索引了,这就是索引的最左匹配原则。
那为什么会这样呢?
索引能够加快查找的原理在于:索引字段是有顺序的,通过二分查找能够快速匹配目标数据。
而对于(a, b) 这类联合索引来说,B+树的结构大概如下:
可以看出,第一个字段a是顺序递增的(1,1,2,2,3,3),而第二个字段b总体上却没有明显的顺序(1,2,1,4,1,2),只是局部有序(a固定的时候b是递增的)。
a = 1 and b = 2
这个组合条件中的b字段可以使用索引;**有的小伙伴还会抛出一个疑问:**如果只考虑 a=1 and b=2
这种组合条件,创建联合索引时该创建(a, b) 还是(b,a)呢?
对于等值查询来说,一般是把区分度高的字段放在前面,像姓别、状态这类区分度很低的字段放后面。用show index from [table_name]可以查看:
这里限定了条件是等值查询,对于(>、<)这类范围查询,索引字段顺序的考量是有些区别的。
**知识点:**范围查询右边的所有列都无法使用索引,在创建索引时应当将范围查询条件放在靠后的位置。
还是之前的(a, b)联合索引,例如这个组合条件:a > 1 and b = 2
, a > 1可以命中索引,但 b=2却无法命中索引。
原因在于:a>1是一个模糊的范围,在这个模糊的范围里,数据结构上b字段是无序的(1,4,1,2)。
那回到我们这次遇到的情况,对于
cal_login in ('8418760','8418764','8418766')and end_time > 1652340600
这类组合条件,后面的end_time > 1652340600 肯定是范围查询,那前者呢?
说到这里,挺惭愧,自认为对索引还算有一定程度的了解,但在遇到这次的问题之前,这个Case我的理解一直是错的。
SQL里的 in
这种语法,看着像范围查询,但是实际动手在线下环境操作去创建一个(cal_login
,end_time
)这种联合索引,上面组合条件中的end_time是可以使用索引的,应该是SQL优化器做了专门的优化。
按照目前网上大部分文章的说法,能确定是范围查询的语法包括这几个 (>、<、between、like)。
一般创建联合索引还会面临的一个选择是如何让分组/排序这种语法命中索引。实际工作中用explain分析慢查询时会遇到这类问题:
extra一栏中的filesort表明使用内存排序,temporary表明由于要排序的记录太多,使用了磁盘临时文件。两种都比较耗资源,后者更甚,一旦使用了磁盘临时文件,这个SQL基本会成为慢查询的常客。
最好的情况是让order by 语句命中索引,相关知识点:
例1, 适合给(a, b)建立联合索引,a=1的时候b相对有序:
SELECT * FROM `table` WHERE a = 1 ORDER BY b;
例2, 带范围查询又带order by 语句的SQL:
SELECT * FROM `table` WHERE a = 1 AND b IN (1,2,3) AND c > 3 ORDER BY c;
假如索引是(a, b, c), 由于b不确定,所以order by c是无法使用索引的,不过c > 3条件可以使用索引。
本文由博客一文多发平台 OpenWrite 发布!