深分页问题
什么是深分页问题
select * from table where uid = {uid} limit 300 30;300 是 offset,30是要返回多少条数据。也就是查询第十页的内容。每页 30 条。也就是得到第 300 条到第 330 条的数据
innodb 会做一件很消耗性能的事情:把第 1 条数据到第 330 条数据全部返回到 server 层,然后根据 offset = 300将前面的 300
条数据抛弃。
还没完,如果涉及到回表操作,330 条数据全部回表查询后才会把前 300 条数据抛弃。这就更加损耗性能了。
如果你在尝试在一张巨型表中explain如上语句,数据库甚至会在type那一栏中显示“ALL”,也就是* *全表扫描**。这是因为* *优化器** ,会在执行器执行sql语句前,判断下哪种执行计划的代价更小。但优化器在看到非主键索引的600w次回表之后,直接摇了摇头,说“还是全表一条条记录去判断吧”,于是选择了全表扫描。
所以,出现深分页时,大概率会导致全表扫描。完完全全的性能杀手。
这是https:
//ramzialqrainy.medium.com/faster-pagination-in-mysql-you-are-probably-doing-it-wrong-d9c9202bbfd8中的一些数据,可以看到随着分页的深入(offset递增),耗时呈指数型上升。
如何解决深分页问题
减少回表损耗
延迟 join
1 | select * from table inner join ( |
Seek Method
深分页的本质原因是,offset越大,需要扫描的行数越多,然后再丢掉,导致查询性能下降。如果我们可以精确定位到上次查询到哪里,然后直接从那里开始查询,就能省去“回表、丢弃”这两个步骤了。我们可以每次记录上一次查询的进度,从该进度接着往查询。例如:
select id from table where uid = '{uid}' and id> {last_id} limit 30;
这也是我们平时最常用的分页方法。但是这个方法有几个弊端,需要我们做一定的取舍:
Seek Method 局限一:无法支持跳页。
传统分页设计下,需要支持直接跳到第X页,SeekMethod 无法实现。 但是目前大部分软件设计都是瀑布流设计,除了一些后台管理、通知公告等业务外,几乎不会支持分页查询。Seek Method 局限二:排序场景下有限制
但大部分的业务的SQL并没有本文中的例子这么简单,至少会多一个条件:按照创建时间/更新时间等排序(大部分情况倒序):1 | select |
如果需要改成瀑布流的话,需要先排序再分页,性能不如上一种。这种情况可以给 time
加索引
1 | select |
这样一眼看去没有什么问题,但是问题是create_time 和
id有一个最大的区别在于ID能保证全局唯一,但是create_time 不能。万一全局范围内create_time
出现重复,那么这个方法是有可能出现丢数据的。如下图所示,当翻页的时候直接用select_time>200的话,可能会丢失数据。
要解决这个问题也有一些方法,笔者尝试过的有:
1.主键字段设计上保证和排序字段的单调性一致。怎么说呢?例如我保证create_time越大的,id一定越大(例如使用雪花算法来计算出ID的值)。那么这样就依旧可以使用ID字段作为游标来改写SQL了2.把<改成<=,这样以后,数据就不会丢了,但是可能会重复。然后让客户端做去重。这样做其实还有一个隐患,就是如果select_time不等于当前时间,而是最大的
create_time,而且相同create_time的数据真的太多了,已经超过了一页。那么永远都翻不了页。
因为 查出来的数据 create_time都是一样的,select_time永远原地踏步。