笔者现在在一家广州大型电商公司上班,最近公司的业务上发生了一件很严重的事情,用户在平台上操作了退货后,却没有快递员上门揽货,快递公司也没有收到相关请求,经过详细排查,发现是慢查询SQL导致的,这个问题影响了1000多单,给用户带来了极差的用户体验,也让我们部门上了公司黑榜,导致整个技术部门收获了一个三星故障,如果一个年度内部门收获两个四星故障,那整个部门的年终奖就没了,三星是比四星还要严重的故障。虽然不是我们小组负责的项目,但是属于同个三级部门,属于隔壁小组搞出来的,整个技术部门对慢查询SQL进行大量排查,其实这个问题一直是有告警出来的,只是技术人员一直无视,才导致了如此严重的事故。
用户退货的流程如下:
用户---》操作退货请求--》系统收到退货请求--》系统向快递公司下达揽收请求--》快递员上门揽货
监控中心和DBA把我们小组负责的系统的慢查询SQL全部整理出来,查询时间超过0.1秒的都被认定是慢查询SQL,全部有150条左右,领导安排我来做慢查询SQL的优化,这些慢查询SQL的优化大体可以划分成以下几类:
可以使用explain查询msyql的执行计划。
1.添加索引
场景:我们有个日志表,这个日志表有两个核心字段,包括result_code和create_time,result_code用来记录业务的操作结果,如果业务执行成功,则result_code=200,占大多数;如果业务执行失败,则result_code=400或其他,占少部分,create_time是记录创建的时间,这个表每天新增的数据量是几百万,create_time是有加索引的,但result_code字段没加索引,导致要查出某段时间业务执行失败的数据时,需要扫描的数据量非常大,从而导致慢查询SQL。
解决办法:给字段result_code加索引。
这里有个前提,那就是result_code=200占多数,result_code =400占小数,所以对字段result_code加索引后,查询result_code=400的性能会提高很多,但如果result_code=200和400的数量相关不大的话,其实也不会有太大的提升。
系统中相当一部分慢查询SQL都是可以添加索引的方式进行优化。
2.改变索引字段
比如有以下SQL:
select count(*) from log where create_time>DATE_SUB(NOW(), INTERVAL 1 DAY)
log中有核心字段result_code和create_time,其中建了联合索引index(result_code,create_time)
这条SQL不会中索引,通过优化添加查询条件的方式命中联合索引,SQL改成
select count(*) from log where result_code IN(200,400) and create_time>DATE_SUB(NOW(), INTERVAL 1 DAY)
3.停掉无用的SQL
有些业务告警配置了查询时间段比较长,比如15天,或者字段使用模糊查询条件like '%XXX%',导致扫描数量量较大,向相关人员确认没该需求后就关闭告警,停掉SQL。
4.添加查询条件
场景:很多页面都有一个查询功能,页面上有很多查询条件中以填写,初次进页面时,如果没有填写一些查询条件或者查询条件中的过滤效果不好,就很容易产生慢查询SQL。
比如页面的初次查询是:
select count(*) from log
如果表log中的数据量达到几千万或者上亿,那么这个查询花费的时间少则几秒,多则十几秒。
解决办法:设置默认的查询字段,比如默认查询一天内的数据
select count(*) from log where create_time>DATE_SUB(NOW(), INTERVAL 1 DAY)
5.程序优化
场景:很多SQL使用模糊查询 like '%XXX%',这种查询是不会中索引的,虽然MySQL5.6.24上InnoDB引擎也支持全文索引,但如果表的数据量比较大的话,全文索引会占用很大的空间。
解决办法:在某些特定场景下,可以在程序中把关键词识别出来,放入单独的字段,并加上索引。
这只是在特定场景才可以生效,而且要修改程序,比较费时。
6.改变引擎优化方向
场景:同时使用id和create_time索引,比如以下SQL
select count(*) from log where create_time>DATE_SUB(NOW(), INTERVAL 1 DAY) and id>0
我们有个定时器是扫描出最近几天执行失败的数据,拿出来进行重试,使用id和创建时间进行过滤,初次循环使用maxId=0,下次循环maxId=maxId+count,但第一次循环时由于是maxId=0,所以就是上面的SQL语句
MYSQL引擎会认为走ID主键索引是最优的,相当于扫描出全表的ID,再使用create_time索引进行过滤,导致查询效率极差,需要300多秒。
优化的办法是,首次查询时取消id>0的条件,第二次查询时才加上id>maxId,这样的首次查询时间就降到2秒多。
7.无法优化。
SQL中的查询字段有中索引,但需要扫描的数据量较大,或者由于使用like '%XX%'导致全表扫描等等,很多这类场景并没有多少可以优化的空间。
8.幽灵事件
遇到有个SQL,大多数情况下是有中索引,花费0.4秒,但有时候是全表扫描,花费4000多秒,我百思不得其解。
SQL的大概结构如下:
select count(*)as ct from business t
inner join order_log ol on ol.no = t.no and ol.type = 'a'
inner join order o on o.no = ol.no and o.type = 'a'
where t.create_time >= DATE_SUB(NOW(), INTERVAL 2 DAY)
and t.create_time < DATE_SUB(NOW(), INTERVAL 1 DAY)
and t.message_body like '%a%' and t.message_body not like '%b%'
and t.result_code = 400
9.MYSQL内存不足
有些SQL,有些情况下查询时间只花了几十毫秒,但有时候花费了两三秒,SQL是有中索引的,DBA回复说是系统的内存不够导致需要将扫描出来的数据放入磁盘,从而使用查询效率低下。