千万级用户场景下的运营系统的复杂SQL调优实战案例

背景: 百万级日活用户的互联网公司,该系统是专门通过各种条件筛选出大量的用户,接着对那些用户去推送一些 消息的,有的时候可能是一些促销活动的消息,有的时候可能是让你办会员卡的消息,有的时候可能是 告诉你有一个特价商品的消息。

因为这种互联网公司,我们已经说过了,用户是日活百万级的,注册用户是千万级的,而且如果还没有 进行分库分表的话,那么这个数据库里的用户表可能就一张,单表里是上千万的用户数据,大概是这么 一个情况。

  1. select id,name from users where id in (select user_id from users_extent_info where latest_login_time < xxxxx )

意思就是说一般存储用户数据的表会分为两张表,一 个表用来存储用户的核心数据,比如id、name、昵称、手机号之类的信息,也就是上面SQL语句里的 users表 另外一个表可能会存储用户的一些拓展信息,比如说家庭住址、兴趣爱好、最近一次登录时间之类的, 就是上面的users_extent_info表。

子查询,里面针对用户的拓展信息表,也就是 users_extent_info查询了一下最近一次登录时间小于某个时间点的用户,这里其实可以是查询最近才登 陆过的用户,也可以查询的是很长时间没登录过的用户 ,然后给他们发送一些push,无论哪种场景, 这个SQL都是适用的。

然后在外层的查询里,直接就是用了id IN字句去查询 id 在子查询结果范围里的users表的所有数据,此 时这个SQL往往一下子会查出来很多数据,可能几千、几万、几十万,都有可能,所以其实一般运行这 类SQL之前,都会先跑一个count聚合函数,看看有多少条。

select count(id) from users where id in(select user_id from users_extent_info where latest_login_time < xxxx)

然后内存里做一个小批量多批次读取数据的操作,比如判断如果在1000条以内,那么就一下子读取出 来,如果超过1000条,可以通过LIMIT语句,每次就从这个结果集里查1000条数据,查1000条就做一 次批量PUSH,再查下一波1000条。

这就是这个案例的一个完整的业务背景和讲解,那么当时产生的问题是什么呢?
是在千万级数据量的大表场景下,上面的SQL直接轻松跑出来耗时几十秒的速度,所以说, 这个SQL不优化是绝对不行了!

系统运行的时候,肯定会先跑一下COUNT聚合函数来查查这个结果集有多少数据,然后再 分批查询。结果就是这个COUNT聚合函数的SQL,在千万级大表的场景下,都要花几十秒才能跑出来, 简直是大跌眼镜,这种性能,系统基本就没法跑了

EXPLAIN SELECT COUNT(id) FROM users WHERE id IN (SELECT user_id FROM users_extent_info
WHERE latest_login_time < xxxxx)

| id | select_type | table | type | key | rows | filtered | Extra |
| 1 | SIMPLE | | ALL | NULL | NULL | 100.00 | NULL |
| 1 | SIMPLE | users | ALL | NULL | 49651 | 10.00 | Using where; Using join buffer(Block Nested Loop) |
| 2 | MATERIALIZED | users_extent_info | range | idx_login_time | 4561 | 100.00 | NULL |

对子查询,是执行计划里的第三行实现的,他清晰的表明,针对users_extent_info,使用了 idx_login_time这个索引,做了range类型的索引范围扫描,查出来了4561条数据,没有做其他的额外 筛选,所以filtered是100%。

接着他这里的MATERIALIZED,表明了这里把子查询的4561条数据代表的结果集进行了物化,物化成了 一个临时表,这个临时表物化,一定是会把4561条数据临时落到磁盘文件里去的,这个过程其实就挺慢 的。

然后第二条执行计划表明,接着就是针对users表做了一个全表扫描,在全表扫描的时候扫出来了 49651条数据,同时大家注意看Extra字段,拿每一行数据的id去判断在不在物化表里,显示了一个Using join buffer的信息,这个明确表示,此处 居然在执行join操作???

接着看执行计划里的第一条,这里他是针对子查询产出的一个物化临时表,也就是,做了一个全表查 询,把里面的数据都扫描了一遍,那么为什么要对这个临时表进行全表扫描呢?

原因就是在让users表的每一条数据,都要去跟物化临时表里的数据进行join,所以针对users表里的每 一条数据,只能是去全表扫描一遍物化临时表,找找物化临时表里哪条数据是跟他匹配的,才能筛选出 来一条结果。

第二条执行计划的全表扫描的结果表明是一共扫到了49651条数据,但是全表扫描的过程中,因为去跟 物化临时表执行了一个join操作,而物化临时表里就4561条数据,所以最终第二条执行计划的filtered 显示的是10%,也就是说,最终从users表里筛选出了也是4000多条数据。

为什么慢

那么这里为什么会跑的这么慢呢?其实很明显了,大家可以想一下,首先他对子查询的结果做了一次物 化临时表,落地磁盘了,接着他还全表扫描了users表的所有数据,每一条数据居然跑到一个没有索引 的物化临时表里再做一次全表扫描找匹配数据。

这里交大家一个技巧,就是在执行完上述SQL的EXPLAIN命令,看到执行计划之后,可以执行一下 show warnings命令,显示如下:
/ select#1 / select count( d2. users . user_id ) AS COUNT(users.user_id) from d2 . users users semi join xxxxxx ; 因为可读性实在不高,大家关注的 应该是这里的semi join这个关键字

MySQL在这里,生成执行计划的时候,自动就把一个普通的IN子句,“优化”成了基 于semi join来进行IN+子查询的操作,这个semi join是什么意思呢?

简单来说,对users表不是全表扫描了么?对users表里每一条数据,去对物化临时表全表扫描做semi join,不需要把users表里的数据真的跟物化临时表里的数据join上。只要users表里的一条数据,在物 化临时表里可以找到匹配的数据,那么users表里的数据就会返回,这就叫做semi join,他是用来筛选 的。

所以慢,也就慢在这里了,那既然知道了是semi join和物化临时表导致的问题,应该如何优化呢?

先别急,做个小实验,执行SET optimizer_switch=’semijoin=off’,也就是关闭掉半连接优化,此时执 行EXPLAIN命令看一下此时的执行计划,发现此时会恢复为一个正常的状态。

就是有一个SUBQUERY的子查询,基于range方式去扫描索引搜索出4561条数据,接着有一个 PRIMARY类型的主查询,直接是基于id这个PRIMARY主键聚簇索引去执行的搜索,然后再把这个SQL语 句真实跑一下看看,发现性能一下子提升了几十倍,变成了100多毫秒!

因此到此为止,这个SQL的性能问题,真相大白,其实反而是他自动执行的semi join半连接优化,给咱 们导致了问题,一旦禁止掉semi join自动优化,用正常的方式让他基于索引去执行,性能那是嗖嗖的。

当然,在生产环境是不能随意更改这些设置的,所以后来我们想了一个办法,多种办法尝试去修改SQL 语句的写法,在不影响他语义的情况下,尽可能的去改变SQL语句的结构和格式,最终被我们尝试出了 一个写法,如下所示:

SELECT COUNT(id)
FROM users
WHERE ( id IN (SELECT user_id FROM users_extent_info WHERE latest_login_time < xxxxx) OR id IN
(SELECT user_id FROM users_extent_info WHERE latest_login_time < -1))

在上述写法下,WHERE语句的OR后面的第二个条件,根本是不可能成立的,因为没有数据的 latest_login_time是小于-1的,所以那是不会影响SQL语义的,但是我们发现改变了SQL的写法之后, 执行计划也随之改变。

他并没有再进行semi join优化了,而是正常的用了子查询,主查询也是基于索引去执行的,这样我们在 线上上线了这个SQL语句,性能从几十秒一下子就变成几百毫秒了

希望大家能认真体会这个SQL调优案例里的方法,其实最核心的,还是看懂SQL的执行计划,然后去分 析到底他为什么会那么慢,接着你就是要想办法避免他全表扫描之类的操作,一定要让他去用索引,用 索引是王道,是最重要的!

亿级数据商品系统SQL 调优实战

·MySQL 在选择索引的时候选择了一个不合适的索引,导致性能极差,引发了慢查询。

告警: 数据库突然涌现出大量的慢查询,因为大量的慢查询,导致数据库执行一个慢查询耗时很久。如果这个时候过来很多数据库请求,就需要开辟出更多的数据库连接,因此此时数据库连接也突然暴增了,而且此时每个连接都执行慢查询,导致每个数据库连接都打满了。

数据库的连接全部打满,没有办法开辟新的连接,导致新的查询发送过来,数据库没办法处理,然后查询就直接阻塞超时了,直接导致了线上商品系统报异常。

select * from products where category = 'xxx' and sub_category = 'xx' 
order by id desc limit xx,xx

其实是一个很稀松平常的SQL语句,他就是用户在电商网站上根据商品的品类以及子类在进行筛选。
这个语句执行的商品表里大致是1亿左右的数据量,这个量级已经稳定了很长时间了,主要也就是这么 多商品,但是上面的那个语句居然一执行就是几十秒,导致数据库连接资源被全部打满,商品系统无法运行,处于崩溃状态。

表是已经对常用的查询字段建立好了索引的, KEY index_category(catetory,sub_category)肯定是存在的,所以基本可以确认上面的SQL绝对是 可以用上索引的。

一旦用上了KEY index_category(catetory,sub_category) 索引,那么按品类和子类在索引里筛选数据快且数据不多。即使是亿级数据执行时间不应该超过1秒钟。

现在跑了 几十秒,肯定就没用到建立好的索引,才这么慢。

explain select * from products where category = 'xxx'  and sub_category ='xx' 
order by id desc limit xx,xx

possible_keys : index_category
实际走的key:primary ,Extra : using where
他其实本质上就是在主键的聚簇索引上进 行扫描,一边扫描,一边 还用了where条件里的两个字段去进行筛选,所以这么扫描的话,那必然就是 会耗费几十秒了!

因此此时为了快速解决这个问题,就需要强制性的改变MySQL自动选择这个不合适的聚簇索引进行扫描 的行为
用force index 走强制索引。

select *  from products force index(index_categoty) 
where category='xxx' and sub_category='xxx'
order by id desc
limit xx,xx

使用上述语法过后,强制让SQL语句使用了你指定的索引,此时再次执行这个SQL语句,会发现他仅仅 耗费100多毫秒而已!性能瞬间就提升上来了!

  • 为什么在这个案例中MySQL默认会选择对主键的聚簇索引进行扫描?
  • 为什么没使用index_category这个二级索引进行扫描?
  • 即使用了聚簇索引,为什么这个SQL以前没有问题,现在突然就有问题了?


    对于这个亿级大表,index_category 二级索引比较大,MySQL会有一个判断,要是去二级索引查到符合条件的一波数据,还需要回表去查询,但是在回表前,必然做完order by id desc limit xx,xx 的操作。

    根据where category=’xx’ and sub_category=’xx’,从index_category二级索引里 查找出了一大波数据。假设几万条数据,此时二级索引里是包含了主键值的,此时就按照 order by id desc 排序语法,对几万条数据基于临时磁盘文件进行fileSort磁盘排序。

排序完 了之后,再按照limit xx,xx语法,把指定位置的几条数据拿出来,假设就是limit 0,10,那么就是把10条 数据拿出来。 拿出来10条数据之后,再回到聚簇索引里去根据id查找,把这10条数据的完整字段都查出来,这就是 MySQL认为如果你使用index_category的话,可能会发生的一个情况。

所以MySQL担心的是,你根据where category=’xx’ and sub_category=’xx’,从index_category二级索引里 查出来的数据太多了,还得在临时磁盘里排序,可能性能会很差,因此MySQL就把这种方式判定为一种 不太好的方式

因此 MySQL 选择换了一个执行方式,直接扫描主键的聚簇索引。因为聚簇索引都是按照id值有序的,所以扫描的时候,直接按照order by id desc 这个倒序顺序就可以了,然后因为limit 0,10 ,就仅仅只要拿到10条数据就行了。

所以他在按顺序扫描聚簇索引的时候,就会对每一条数据都采用Using where的方式,跟where category=’xx’ and sub_category=’xx’条件进行比对,符合条件的就直接放入结果集里去,最多就是放 10条数据进去就可以返回了。

此时MySQL认为,按顺序扫描聚簇索引,拿到10条符合where条件的数据,应该速度是很快的,很可能 比使用index_category二级索引那个方案更快,因此此时他就采用了扫描聚簇索引的这种方式!

实际这个SQL之前在线上系统运行一直没有什么问题,即使扫描聚簇索引的方案,平时也不慢。

但为什么突然就报慢查询了呢?

原因也很简单,其实就是因为之前的时候,where category=’xx’ and sub_category=’xx’这个条件通常 都是有返回值的,就是说根据条件里的取值,扫描聚簇索引的时候,通常都是很快就能找到符合条件的 值以及返回的,所以之前其实性能也没什么问题。

但是后来可能是商品系统里的运营人员,在商品管理的时候加了几种商品分类和子类,但是这几种分类 和子类的组合其实没有对应的商品 ; 也就是说,那一天晚上,很多用户使用这种分类和子类去筛选商品,where category=’新分类’ and sub_category=’新子类’这个条件实际上是查不到任何数据的!

所以说,底层在扫描聚簇索引的时候,扫来扫去都扫不到符合where条件的结果,一下子就把聚簇索引 全部扫了一遍,等于是上亿数据全表扫描了一遍,都没找到符合where category=’新分类’ and sub_category=’新子类’这个条件的数据。

才导致这个SQL语句频繁的出现几十秒的慢查询,进而导致MySQL连接资源打满,商 品系统崩溃!

数十亿数量级评论系统的SQL调优实战

电商场景下非常普遍的商品评论系统的一个SQL 优化,这个商品评论系统的数据量非常大,拥有多达十亿量级的评论数据,所以当时对这个评论数据 库,我们是做了分库分表的,基本上分完库和表过后,单表的评论数据在百万级别。

每一个商品的所有评论都是放在一个库的一张表里的,这样可以确保你作为用户在分页查询一个商品的 评论时,一般都是直接从一个库的一张表里执行分页查询语句就可以了。

场景: 在电商网站里,有一些热门的商品,可能销量多达上百万,商品的评论可能多达几十万 条。然后呢,有一些用户,可能就喜欢看商品评论,他就喜欢不停的对某个热门商品的评论不断的进行 分页,一页一页翻,有时候还会用上分页跳转功能,就是直接输入自己要跳到第几页去。
所以这个时候,就会涉及到一个问题,针对一个商品几十万评论的深分页问题。

select *  from comments where product_id = 'xx' and is_good_comment = '1' order by id desc limit 100000,20

// 优化- 我的想法
select *  from comments where product_id ='xx' and is_good_comment = '1' and id > 100000 order by id desc limit 20

用户选择了查看某个商品的评论,因此必须限定Product_id,同时还选了只看 好评,所以is_good_commit也要限定一下
接着他要看第5001页评论,那么此时limit的offset就会是(5001 - 1) * 20,其中20就是每一页的数量, 此时起始offset就是100000,所以limit后100000,20

对这个评论表呢,最核心的索引就是一个,那就是index_product_id,所以对上述SQL语句,正常情况 下,肯定是会走这个索引的,也就是说,会通过index_product_id索引,根据product_id =’xx’这个条件 从表里先删选出来这个表里指定商品的评论数据。
那么接下来第二步呢?当然是得按照 is_good_comment=’1’ 条件,筛选出这个商品评论数据里的所有 好评了!但是问题来了,这个index_product_id的索引数据里,并没有is_good_commet字段的值,所 以此时只能很尴尬的进行回表,筛选数据。
也就是说,对这个商品的每一条评论,都要进行一次回表操作,回到聚簇索引里,根据id找到那条数 据,取出来is_good_comment字段的值,接着对is_good_comment=’1’条件做一个比对,筛选符合条 件的数据
那么假设这个商品的评论有几十万条,岂不是要做几十万次回表操作?虽然每次回表都是根据id在聚簇 索引里快速查找的,但还是架不住你每条数据都回表啊!!!
接着对于筛选完毕的所有符合WHERE product_id =’xx’ and is_good_comment=’1’条件的数据,假设有 十多万条吧,接着就是按照id做一个倒序排序,此时还得基于临时磁盘文件进行倒序排序,又得耗时很 久。 排序完毕了,就得基于limit 100000,20获取第5001页的20条数据,最后返回。
这个过程,因为有几十万次回表查询,还有十多万条数据的磁盘文件排序,所以当时发现,这条SQL语 句基本要跑个1秒~2秒。

select *  from comments a,(select id from comments where product_id = 'xx' and is_good_comment = '1' order by id desc limit 100000,20)b where a.id = b.id

上面那个SQL语句的执行计划就会彻底改变他的执行方式,他通常会先执行括号里的子查询,子查询反 而会使用PRIMARY聚簇索引,按照聚簇索引的id值的倒序方向进行扫描,扫描过程中就把符合WHERE product_id =’xx’ and is_good_comment=’1’条件的数据给筛选出来。

比如这里就筛选出了十万多条的数据,并不需要把符合条件的数据都找到,因为limit后跟的是 100000,20,理论上,只要有100000+20条符合条件的数据,而且是按照id有序的,此时就可以执行根 据limit 100000,20提取到5001页的这20条数据了。 (本来数据就是按照id排序,所以当扫描到100000+20 条符合条件的数据时,就可以不用在扫描了。返回临时表 20条数据) 接着对20条数据遍历,每一条数据都按照id去聚簇索引里查找一下完整数据,就可以了。

所以针对我们的这个场景,反而是优化成这种方式来执行分页,他会更加合适一些,他只有一个扫描聚 簇索引筛选符合你分页所有数据的成本,你的分页深度越深,扫描数据越多,分页深度越浅,那扫描数 据就越少,然后再做一页20条数据的20次回表查询就可以了

当时我们做了这个分页优化之后,发现这个分页语句一下子执行时间降低到了几百毫秒了,此时就达到 了我们优化的目的。

千万级数据删除导致的慢查询优化实践

背景:有人删除了千万级别的数据,结果导致了频繁的慢查询。

当时是从线上收到大量的慢查询告警开始的,当我们收到大量的慢查询告警之后,就去检查慢查询的SQL,结果发现不是什么特别的SQL,这些SQL语句主要都是针对一个表的,同时也比 较简单,而且基本都是单行查询,看起来似乎不应该会慢查询。

所以这个时候我们是感觉极为奇怪的,因为SQL本身完全不应该有慢查询,按说那种SQL语句,基本上 都是直接根据索引查找出来的,性能应该是极高的。

那么有没有另外一种可能,慢查询不是SQL的问题,而是MySQL生产服务器的问题呢?
这里给大家解释一下,实际上个别特殊情况下,MySQL出现慢查询并不是SQL语句的问题,而是他自己 生产服务器的负载太高了,导致SQL语句执行很慢。

给大家举个例子,比如现在MySQL服务器的磁盘IO负载特别高,也就是每秒执行大量的高负载的随机 IO,但是磁盘本身每秒能执行的随机IO是有限的。 结果呢,就导致你正常的SQL语句去磁盘上执行的时候,如果要跑一些随机IO,你的磁盘太繁忙了,顾 不上你了,导致你本来很快的一个SQL,要等很久才能执行完毕,这个时候就可能导致正常SQL语句也 会变成慢查询!

所以同理,除了磁盘之外,还有一个例子就是网络,也许网络负载很高,就可能会导致你一个SQL语句 要发送到MySQL上去,光是等待获取一个跟MySQL的连接,都很难,要等很久,或者MySQL自己网络 负载太高了,带宽打满,带宽打满了之后,你一个SQL也许执行很快,但是他查出来的数据返回给你, 网络都送不出去,此时也会变成慢查询。

另外一个关键的点就是CPU负载,如果说CPU负载过高的话,也会导致CPU过于繁忙去执行别的任务 了,没时间执行你这个SQL语句,此时也有可能会导致你的SQL语句出现问题的,所以这个大家也得注 意。

所以说慢查询本身不一定是SQL导致的,如果你觉得SQL不应该慢查询,结果他那个时间段跑这个SQL 就是慢,此时你应该排查一下当时MySQL服务器的负载,尤其看看磁盘、网络以及CPU的负载,是否 正 常。
如果你发现那个时间段MySQL生产服务器的磁盘、网络或者CPU负载特别高,那么可能是服务器负载导 致的问题 (但是这个慢应该是针对所有执行的SQL 吧?要慢也是所有的都很慢)

举个例子,当某个离线作业瞬间大批量把数据往MySQL里灌入的 时候,他一瞬间服务器磁盘、网络以及CPU的负载会超高。 此时你一个正常SQL执行下去,短时间内一定会慢查询的,针对类似的问题,优化手段更多的是控制你 导致MySQL负载过高的那些行为,比如灌入大量数据,最好在凌晨低峰期灌入,别影响线上系统运行。
结果奇怪的是,当时我们看了下MySQL服务器的磁盘、网络以及CPU负载,一切正常,似乎也不是这个 问题导致的。

今天我们先站在当时的角度,给大家分析我们的头两步排查手段,一个是检查SQL是否有问题,主要就 是看他的执行计划,这个我们之前都讲过了,另外一个是检查MySQL服务器的负载,今天我们也说明了 背后的一些知识
那么在这两种办法都不奏效之后,下一次我们就要给大家讲当时我们排查问题的第三步,就是用MySQL profilling工具去细致的分析SQL语句的执行过程和耗时, 这个工具可以对SQL语句的执行耗时进 行非常深入和细致的分析 。

首先要打开这个profiling,使用set profiling=1这个命令,接着MySQL就会自动记录查询语句的 profiling信息了。
此时如果执行show profiles命令,就会给你列出各种查询语句的profiling信息,这里很关键的一点,就是他会记录下来每个查询语句的query id,所以你要针对你需要分析的query找到对他的query id,我 们当时就是针对慢查询的那个SQL语句找到了query id。

然后就可以针对单个查询语句,看一下他的profiling具体信息,使用show profile cpu, block io for query xx,这里的xx是数字,此时就可以看到具体的profile信息了
除了cpu以及block io以外,你还可以指定去看这个SQL语句执行时候的其他各项负载和耗时,具体使用 方法,大家自行网上搜索就行了,并不难
他这里会给你展示出来SQL语句执行时候的各种耗时,比如磁盘IO的耗时,CPU等待耗时,发送数据耗 时,拷贝数据到临时表的耗时,等等吧,反正SQL执行过程中的各种耗时都会展示出来的。
这里我们当时仔细检查了一下这个SQL语句的profiling信息,重点发现了一个问题,他的Sending Data 的耗时是最高的,几乎使用了1s的时间,占据了SQL执行耗时的99%,这就很坑爹了。
因为其他环节耗时低是可以理解的,毕竟这种简单SQL执行速度真的很快,基本就是10ms级别的,结 果跑成了1s,那肯定Sending Data就是罪魁祸首了!

Sending Data是 :
MySQL的官方释义如下:为一个SELECT语句读取和处理数据行,同时发送数据给客户端的过程,简单 来说就是为你的SELECT语句把数据读出来,同时发送给客户端。

可是为什么这个过程会这么慢呢?profiling确实是提供给我们更多的线索了,但是似乎还是没法解决掉 问题。但是毕竟我们已经捕获到了第一个比较异常的点了,就是Sending Data的耗时很高!请大家记住 这个线索

有时候针对MySQL这种复杂数据库软件的调优过程,就跟福尔摩斯破案一样,你要通过各种手段和工具 去检查MySQL的各种状态,然后把有异常的一些指标记下来,作为一个线索,当你线索足够多的时候, 往往就能够汇总大量的线索整理出一个思路了,那也就是一个破案的时刻了!

接着我们又用了一个命令:show engine innodb status,看一下innodb存储引擎的一些状态,此时 发现了一个奇怪的指标,就是history list length这个指标,他的值特别高,达到了上万这个级别。

大家应该还记得之前我们讲解过的MVCC机制吧?MVCC机制,说穿了就是多个事务在对同一个数据, 有人写,有人读,此时可以有多种隔离级别,这个大家应该还记得吧。 至于这个MVCC和隔离级别的实现原理,跟一个Read View机制是有关系的,同时还有一个至关重要的 机制,就是数据的undo多版本快照链条。

所以当你有大量事务执行的时候,就会构建这种undo多版本快照链条,此时history list length的值就 会很高。然后在事务提交之后,会有一个多版本快照链条的自动purge清理机制,只要有清理,那么这 个值就会降低。

一般来说,这个值是不应该过于高的,所以我们在这里注意到了第二个线索,history list length值过 高!大量的undo多版本链条数据没被清理!推测很可能就是有的事务长时间运行,所以他的多版本快照不能被purge清理,进而导致了这个history list length的值过高!

第二个线索Get!基本可以肯定的一点是,经过两个线索的推测,在大量简单SQL语句变成慢查询的时 候,SQL是因为Sending Data环节异常耗时过高,同时此时出现了一些长事务长时间运行,大量的频繁 更新数据,导致有大量的undo多版本快照链条,还无法purge清理。

但是这两个线索之间的关系是什么呢?是第二个线索推导出的事务长时间运行现象的发生,进而导致了 第一个线索发现的Sending Data耗时过高的问题吗?可是二者之间的关系是什么呢?是不是还得找到更 多的线索还行呢?
简单来说,当时经过排查,一直排查到117讲末尾的时候,发现有大量的更新语句在活跃,而且有那种 长期活跃的超长事务一直在跑没有结束,结果一问系统负责人,发现他在后台跑了一个定时任务,定时 清理数据,结果清理的时候一下子清理了上千万的数据。

这个清理是怎么做的呢?他居然开了一个事务,然后在一个事务里删除上千万数据,导致这个事务一直 在运行,所以才看到117讲末尾发现的一些现象

然后呢,这种长事务的运行会导致一个问题,那就是你删除的时候仅仅只是对数据加了一个删除标记, 事实上并没有彻底删除掉。此时你如果跟长事务同时运行的其他事务里在查询,他在查询的时候是可能 会把那上千万被标记为删除的数据都扫描一遍的。

因为每次扫描到一批数据,都发现标记为删除了,接着就会再继续往下扫描,所以才导致一些查询语句 会那么的慢

那么可能有人会问了,为什么你启动一个事务,在事务里查询,凭什么就要去扫描之前那个长事务标记 为删除状态的上千万的垃圾数据呢?按说那些数据都被删除了,跟你没关系了,你可以不用去扫描他们 啊!

这个问题的关键点就在于,那个删除千万级数据的事务是个长事务!
也就是说,当你启动新事务查询的时候,那个删除千万级数据的长事务一直在运行,是活跃的!所以大 家还记得我们之前讲解MVCC的时候,提到的一个Read View的概念么?MVCC是如何实现的?不就是基 于一个Read View机制来实现的么?

所以正是因为这个长事务一直在运行还在删除大量的数据,而且这些数据仅仅是标记为删除,实际还没 删除,所以此时你新开事务的查询是会读到所有被标记为删除的数据的,就会出现千万级的数据扫描, 才会造成慢查询!
针对这个问题,其实大家要知道的一点是,永远不要在业务高峰期去运行那种删除大量数据的语句,因 为这可能导致一些正常的SQL都变慢查询,因为那些SQL也许会不断扫描你标记为删除的大量数据,好 不容易扫描到一批数据,结果发现是标记为删除的,于是继续扫描下去,导致了慢查询!

所以当时的解决方案也很简单,直接kill那个正在删除千万级数据的长事务,所有SQL很快会恢复正常, 从此以后,对于大量数据清理全部放在凌晨去执行,那个时候就没什么人使用系统了,所以查询也很 少。