前不久博主做了一个新需求,需求上线之前需要刷一遍数据表中的数据,刷数据的业务逻辑也比较简单,就是简单的往一个json字段中再插入一个字段,但是呢,这张表中的数据量比较大,数据记录高达六千多万条。
其实像这种业务逻辑,博主以前也处理过,我们的做法一般都是分批查询表中的数据进行处理(至于为什么要分批处理,简单来说就是六千多万的数据基本不可能一次性查出来放到内存中,服务器会冒烟的~,所以一般都是一批一批的处理,整个处理方法也不能加事务,否则对数据库的压力依然是巨大的),于是博主就写了个sql,使用limit分页分批次获取表中的数据进行处理。逻辑写完之后博主先是把测试库的数据刷了一遍(刷之前记得备份,业务逻辑执行错误的时候可以恢复数据库),没啥毛病,执行效率也还行,然后博主就把代码提交了,等着上线前调下接口就好了
PS:博主当时的sql写法大概是这样的:
select * from 表名 [where条件] order by id desc limit 偏移量,记录数;
~~
到了上线的时候,调用接口,就开始看日志(这里因为业务处理的时间会很长,接口会响应超时,所以不需要看接口返回,直接看日志就好了)。刚开始一切正常,慢慢的就开始不对劲了,limit的记录数都是一样的,每次都是取一样多的记录,可执行效率却越来越慢了,到后面直接就查询超时,接口直接崩了。那为什么之前在测试数据库没有测出来呢,因为测试数据库的数据量只有几十万,跟正式数据库完全不是一个量级,而博主当时也没有意识到这个问题,所以这就导致了惨剧的发生。
经过测试发现,每次循环执行的sql都基本一致,变动的只有limit的偏移量,偏移量会随着循环次数越多而越来越大。
使用"EXPLAIN"关键字对sql进行分析,发现当limit的偏移量越来越大时,“rows”也越来越大,而这个”rows“代表的是mysql预估的执行这条sql所需要扫描或返回的记录条数,当“rows”的值越大,代表这条sql所需要读取的记录数越多,也就代表着执行耗时会越来越长,性能越来越差。
那这里为什么rows的值会越来越大呢?
其实这就跟limit的原理有关了,当<limit m,n>时,就是先读取前面m+n条记录,然后抛弃前m条,读后面n条想要的,所以m越大,偏移量越大,rows也就越来越大。
那如果想解决这个问题的话,其实就是减少这条sql执行时所需要扫描的记录数。
PS:以下都是基于sql使用到索引的情况下分析,如果sql未使用到索引,那不管怎么优化,都是走全表扫描,性能低下
优化后的sql:
select * from 表名 where id > 上一次查询id最大值(第一次可设置为0) and ... order by id desc limit 记录数;
这里的核心点就是要根据id进行排序,配合业务代码,记录前一次查询取结果集的id最大值,用前一次查询结果集的id最大值替换后一次查询的where id条件值,主动过滤掉不需要的记录,以此来减少sql执行时所需要扫描的记录数,达到优化的效果。
这样做虽然“rows”的基础数增加了,但是随着id值越来越大,跳过的数据越来越多,“rows”的值却并不会产生变化,以此来稳定这条sql的性能。
原理也非常简单,当 limit n 时,读取的记录行数都是固定的,而对比上面的limit m,n,此时需要抛弃的前m条记录已经被where条件筛选出去了,每次都只需要读n条记录,所以"rows"的值是固定的,并不会产生变化。
上面优化后的sql中,order by 的字段是主键id-聚簇索引,但博主经过大量测试发现,如果order by 一个非聚簇索引字段,性能比order by 一个聚簇索引字段性能会更好,但具体原理还没搞清楚,同学们斟酌使用。
最后再给同学们一点建议,像这种刷数据的业务逻辑最好是做成幂等的,也就是说不管业务逻辑执行多少遍,最后呈现的结果都是一致的,博主这里的业务逻辑就是幂等的,所以尽管第一次更新失败也没关系,调整好接口逻辑依然可以从头再刷一遍,并不会影响最后的结果。