这么分页,小心有坑
背景
生产上有个导报表功能,工作了很长一段时间一直都很稳,没出现过什么问题,最近运营同学突然反馈导出来的数据和实际的对不上,经过排查发现导出的数据有重复,也有的没导出来。
由于我们提前生成好数据(每天会truncate重新生成),所以导出的逻辑非常简单,不需要关联很多表捞数据,只需要从一张表查即可,这个表的数据量不大,发生问题时7800条左右,查询的sql也非常简单,可以选择条件导出知道时间段的数据,如下:
SELECT * FROM t_report WHERE repayment_time > 1622390400000 AND repayment_time <= 1624032000000 limit 1000,1000
为了防止一次性导出太多数据,所以我们做了分页,每次查1000条,并且repayment_time字段上建了索引。
经过一顿排查后,发现程序没啥问题,但数据也确实是重复了,也漏了一些...
分析
我们对比了实际数据,确实有问题,有的数据出现两次,有的没出现。如下:
有一条数据在第6页查出,在第8页又被查出。我们再次确认了程序,还是没问题,用arthas观察了分页的入参也都是正确的。
那么直接拿sql出来到数据库查询呢?发现真有问题...,确实在第6页和第8页出现。那么可以确定就是sql有问题了,这么简单的sql查数据居然重复了...
仔细看数据可以发现,在第6页的时候,查出来的数据是按照repayment_time字段排序,而第8页则是按照id进行排序,排序方式不同分页查出来的数据肯定会不同。
问题已经定位到,就是在分页过程中,mysql偷偷改了排序的规则。我们可以看不同页的执行计划如下,由于每天生成的数据不同,这里我改了页数。
那么同样的sql为什么会使用不同的排序,生成不同的执行计划呢?
我们知道mysql在使用索引的时候,是会根据数据的分布进行的,也就是mysql会考虑索引的效果,如果效果好才使用。我们常说性别字段不适合建索引,就是因为这种字段的区分度很低,mysql宁愿全表扫描也不会使用这种索引。使用show index from table可以查看索引信息,cardinality就是字段的区分度,通常该值越大越适合建索引。
我们说到mysql会考虑到数据的分布,虽然我们上面的repayment_time字段是建索引,运营当时的查询条件刚好覆盖了整个表的数据,当翻到第8页时,整个表总共也就8页,就是要查全部数据了,mysql此时会放弃使用索引,进行全表扫描。
前面的页使用到索引,默认会根据索引字段进行排序,而后面页没使用索引,默认就是根据id进行排序。
不一定是最后一页才生成不同的执行计划,比如我们的表有10w条数据,可能查到95000条数据的时候,排序就变了,也就是说mysql认为几乎要查全部数据,就会改变执行计划。
解决方式也很简单,我们可以显示指定排序方式,这样每次生成的执行计划都是一样的,如上的sql,改为:
SELECT * FROM t_report WHERE repayment_time > 1622390400000 AND repayment_time <= 1624032000000 order by repayment_time,id limit 1000,1000
注意加了order by需要考虑file sort问题,排序字段没有使用索引可能会导致性能问题。
分页写法开发有时候很容易漏掉order by,这种最好在需求阶段就和产品确认清楚,这样可以在开发和测试阶段就通过数据验证需求,当然开发也要考虑数据分布,比如数据量,有多少重复数据等,以后记得这个坑,避免再次掉入哦。
更多分享,欢迎关注我的github:https://github.com/jmilktea/jtea