浅谈深分页问题

曾经面试被问到怎么解决深分页问题,因为以前项目用来es,所以重点问的是es的深分页问题,问我怎么解决深分页的问题,我的回答就是没解决。因为我觉得深分页就解决不了(先别浮现出各种网上解决深分页的方式,然后反驳这句话)

与其讨论什么从技术上去解决,还不如换个解读想下:

  1.  什么样的场景下会触发深分页?
  2. 当真的出现深分页问题的时候,如何实现系统的自我保护?

对第一个问题:我觉得就两种情况:第一:正常的业务流量,用户真的闲得蛋疼了,真的就一页一页的往下翻,翻到了几千上万页(只有闲得蛋疼的才会去做这件事情,否则根本没有时间和耐心)。第二:非正常业务流量,被爬了之类的。

那么针对这两种场景要怎么解决,我的答案还是不解决,我需要做的就是真的出现了做好系统保护就好,比如当分页到了100页,就不提供服务了,或者参考ES对深分页的保护。

下面从mysql的分页和ES的分页来感性分析下,所谓的解决深分页问题,到底解决了没有。本文往下也只是分析了几种网上常见的几种方式,不代表所有。如果有更好的方式,或者针对某个特定场景有比较好的方式或者理解不对的地方,欢迎补充,学习。。。

mysql的深分页问题分析

有如下表t:id是自增主键,然后在c上上是有一个普通索引,d上没有索引

所谓的深分页就是对于试用limit m,n这种来实现分页查询的时候,需要将满足条件的m*n条数据都查询出来,然后进行排序,然后再取第(m-1)*n到第m*n这n条数据。当m非常大的时候,那么需要参与排序的数据就会非常多,导致查询结果会非常慢。

对于查询,使用上不同的索引,查询流程也不一样,那么查询代价也就不一样

走主键索引

第一种是:条件中有id的,会使用上主键索引定位的,比如select * from t where id>5 order by id asc limit 1000,20

ps:B+树是个m-n树,一个中间节点中保存了m个数据,n个指向子节点的指针,中间节点的m个数据是排序的,并且n个指针指向的子节点中的数据和父节点中的这m个数据是有大小关系的,这样才能形成索引,这里就示意一下.

这种情况的查询过程:

1. 因为where id>5,能够用上主键索引定位,所以第一步在第一层节点的m=2个数据中,使用二分法查找id=5的数据,发现5是大于0,小于10的,所以去访问0和10中间的这个指针指向的子节点。

2. 于是访问到中间这个节点,发现是叶子节点了,并且id=4,不满足id>5的条件,所以继续向右遍历

3. 向右遍历到id=5,发现还是不满足id>5的条件,所以继续向右遍历,依次类推。

4. 当遍历到id=10的这条数据的时候,发现是满足条件的。但是由于这里是按照主键排序的,然而主键索引上的数据天然就是有序的,所以这里就不需要排序。那就继续向右遍历就好,知道遍历到满足id>5后端面的第1000*20条,然后继续向右遍历20条,那这20条就是最后返回的结果。

但是如果,这里不是order by id,而是order by d。那么在遍历到id=10的时候,只能说这条数据可能出现在结果集中,所以这个时候是将id=10这条数据放到了sort buffer中,然后继续向右遍历,将满足条件的1000*20+20条数据都放到sort buffer中,然后在sortbuffer中进行排序,然后取第1000*20+1到1000*20+20这20条数据返回。如果sort buffer放不下这1020条数据,就会涉及到file sort。

另外一种就是走主键索引进行全表扫描的,比如select * from t where d>5 order by id asc limit 1000,20

对于这种情况,只是说用不上主键索引去定位,只能全表扫描,那就是直接从主键索引的第一个叶子节点,即id=0开始向右遍历,没遍历一个节点,取出字段d,然后检查是否满足条件。同样的,如果oder by id 那就不用排序,直接记录遍历到了多少条满足条件的记录,然后最终返回第1000*20+1到1000*20+20这之间的20条数据。如果是order by d,那同样是需要额外的排序的。

对于这种情况,对于深分页的耗时主要来自两个方面:

1. 遍历的数据会增多,是需要扫描的数据比较多:共m*n+n条

2. 潜在的对m*n+n条排序

ps:如果是select * from  t where id>5 order by c  能用上c索引来避免排序么?答案mysql的优化器不会使用c这个索引,感性分析下:看如果使用索引c省下的是什么,而需要额外付出的代价是什么

1.扫描条数会变少么?不一定。因为索引c是按照c字段排序的,如果id<=5的数据,按照c排序后,都在第1000*20+20之后,那确实是会少扫描一些。但这个太具有随机性了

2. 能避免排序么?可以。因为c本身是有序的,且如果5.7后有索引下推功能,在判断id>5的时候,不需要回表,直接在索引c上就可以判断了。但如果是order by d>5,那每次判断就需要回表,那就非常坑爹了。

3. 最后取20条数据的时候,需要到主键索引回表20次。

这么分析着,好像走索引c扫描更高效些,但是没有具体去测试过,而explain的结果也是没有走索引c(我测试的数据是比较少的,也可能跟数据量有关)

走辅助索引

select * from t where c>5 and id>5 order by c asc limit 1000,20

1. 因为where c>5,发现是可以用上索引c的,所以利用索引c定位到叶子节点c=4(定位过程和主键是一样的,不多说)

2. 当定位的索引c叶子节点c=4后。在没有索引下推功能前,这个时候是拿到c=4对应的id=4,然后去主键索引回表,然后取出id字段,然后判断是否满足id>5的条件;而对于有索引下推功能的版本,这个时候就不需要回报,直接判断id>5的条件。但是如果这里是c>5 and d>5, 那就嗝屁了,没遍历一个节点,都需要一次回表,才能判断d是否满足条件。

ps:如果是个联合索引(c,d,e),然后条件中有where c>5 and e>5,我们都知道这种情况是用不上索引e的,但是可以用上索引c,这个结论也只是在没有索引下推功能的时候适用,在有索引下推的时候,判断e>5是不需要回表的,利用缩影(c,d,e)上的e字段就可以完成判断了,所以这种情况正确结论是:使用c字段完成定位,使用e字段实现索引下推避免回表。

3. 通过判断c=4这个数据不满足要求,于是继续向右遍历。发现c=5这个也不满足,继续向右,c=10的记录是满足的。

4. 因为这个时候order by c,正好c是有序的,所以继续向右边遍历边判断,知道找到满足条件的1000*20+1到1000*20+20条数据为止。ps:如果是order by id,那就只能使用只能将满足c>5 and id>5的记录放到sort buffer,知道满足1000*20+20条数据后进行排序;如果是order by d, 那这个时候可能就需要回表,取出字段d一起放到sort buffer(这里说明一下,不一定只是放了字段d,有可能将select 后需要的字段都放到sort buffer,因为这样就可以不用回表了,这个主要看优化器sort buffer的大小等绝对实用的排序方式)

5. 然后对着20条数据回表操作,获得具体的数据返回。ps:这个时候其实mysql内部可能是对着20条数据进行排序的,这样回表的效率会高很多。

从这个过程看,深分页耗时在哪些地方:

1. 和主键索引上的扫描一样,需要扫描大量数据。limit m,n,需要在索引c上至少扫描m*n+n条数据。之所以是至少:如果是个联合索引(c,d,e)这种情况,而where条件是c和e,用上了索引下推,但是扫描的行数可能就大于m*n+n条了

2. 如果用不上索引下推,即where条件中,除了辅助索引上的字段以及主键以外,还有其他条件,比如where c>5 and id>5 and d>5,那么每次遍历都需要回表。

3. 同样是排序。用不上辅助索引的排序,就需要用到排序算法排序,而分页越深,可能就会用到更慢的file sort。

总结起来,深分页的问题就三个:

1. 扫描行数比较多

2. 排序

3. 回表

深分页优化

知道了深分页耗时的主要点在哪儿,那优化思路就无外乎是避免这几个耗时点。

记录上一页的最大/最小id

下一页:

select * from t where ... and id>minId limit 1,20

上一页:

select * from t where ... and id<maxId limit 1,20

在每次返回数据的时候,都将当前页的最大id和最小id返回给前端,然后前端在请求上一页/下一页数据的时候,带上这个minId和maxId,这样使用id进行过滤,这样能有效避免大量数据排序问题。但是是不是能够极大减少扫描行数,以及减少回表,这个要看具体场景。比如 where c>5 and d>5 and id>maxId,那么对撒秒行数和回表没啥帮助,但是排序确实不用对大量数据排序了。所以,这只是一个通用思路,即使是用这种方式,还是需要注意对索引的情况,避免即使使用了这种方式,效果并不好的情况

局限:

1. 依赖于自增id。

2. 业务场景有限,只能支持上一页/下一页的查询,不于支持任意页的跳转

3. 排序受限。没法任意指定排序字段,排序字段只能是一个自增的字段

利用子查询

这个思路和第一种也是一样的,只是说maxId和minId不是通过透传的,而是通过子查询查询出来的。

select * from t where id >= (select id from t where 。。。 limit 1000,1) limit 20;

或者使用jion的方式:select * from t as a join (select id as t_id from t limit 1000,1) as b on a.id=b.id

感性认识下:

select * from t where ... limit 1000,20 有深分页问题,那为啥子查询的select id from t where 。。。 limit 1000,1就没了么?为啥这个就是优化了呢?

看着两个sql的区别:

1. selelct 后的字段不一样,第一个select后是业务需要的字段;而子查询的select 只是id。

2. limit后的页大小不不一样,第一个sql是实际的业务页的大小,子查询的页码是1。(这里limit 1的作用就是获得minId和maxId)

从这两个区别上看有哪些优化效果:

1. 子查询中因为只是需要id,那么可以通过优化辅助索引,让这个子查询用上覆盖索引,让深分页更高效些。那么就可以大量减少回表。好像也就仅此而已了,其他的我确实没太get到有什么优化效果。

 

回过来看,不管是哪种方式,真的解决了深分页问题么?

1. 第一种,我觉得只是业务妥协规避调了,但是针对一些稍微复杂的查询维度,这个索引也很难建,优化就比较麻烦,所以生产系统中难有用武之地。

2. 第二种,从最终执行效率上看,在深分页场景下有些优化,但说实话,要通过建索引达到想要的优化效果,这个索引也是比较麻烦的。话说回来,都到了这个份上了,mysql难以支撑业务的查询要求了。

换个说法就是mysql它的长处在数据管理上,不在搜索上,那就别强求了,让专业的去做专业的事情吧,ES也是开源免费的。

ES的深分页

ES提供的分页方法是from+size,从原理上,和mysql的limit效果是一模一样。只是说对于分页场景,因为ES是一个分布式系统,最后的二次排序,然后取数据是在一个协调节点上完成的(想想mysql的分库分表的场景下,是不是也有一个类似的角色呢)。所以ES的深分页问题就不多说了,提一下的是ES本身是对深分页有保护机制的,默认情况下,各个shard返回给协调节点的document数量超过1w的时候会直接报错,这个值是有参数可调整的。

很多人说es解决深分页的问题是scroll,但是我真的没太明白scroll接口怎么避免深分页的。scroll接口的基本原理和介绍的mysql的maxId这种差不多,类似有个游标,上一次查询记住这次查询的位置,然后下次从这个位置开始区下一批数据。所以说,对ES的scroll接口来说,就是要"记住"每一次查询,而且,一旦一个scroll context创建,利用这个context的scroll接口查询的数据对变更是无感的(简单理解就是再创建scroll context那一刻对数据做了快照,后面都是基于这个快照来查询的)。

scan-scroll无非是不需要特殊排序的scroll(已经废弃了,不指定排序就好);slice-scroll无非是多线程scroll,但本质上还是一样的。

要使用scroll来实现分页查询,那怎么搞,给每个session开一个scroll context,而且还得记录这个对应关系,否则人家点下一页的时候,找不到该从哪儿"滚"了,而且记录下scroll context,对后面的变更无感,查不到变更的数据的,所以我理解这种方式是解决不了深分页的。

那scroll解决的是什么问题呢?我理解是批量取数据的问题,当要分批取大量数据的时候,别用from+size的方式去分批,这就容易形成深分页问题,应该用scroll接口。而往往这种批量取数的场景对少量的变更也不那么感冒,所以可以用这种方式。

还是回到最开始说的问题,为什么不换个角度思考深分页的问题:

1. 在真实的业务场景中,是否真的会出现深分页

2. 当出现深分页问题的时候,系统能否自我保护,不至于拖垮系统(ES已经实现了)

3. 如果真的就触发了ES的自我保护,对于业务搜索来说,就给友好提示,比如:换个条件试试/选更多条件试试等等,让用户知道该怎么做能满足自己的快速找到数据的诉求

4. 对于搜索来说,粗浅点理解无非是关注两个问题:召回率和准确率。前者说的是查询的时候要尽可能返回数据;后者说的要尽可能准确的返回数据,不要出现用户搜索输入皮鞋,返回列表是吹风机。那么对于一个业务场景上的搜索(抛开那些专业的搜索推荐模型,反正我也不懂),那么我们的目标更简单,让用户快捷方便的定位到自己想要的那条数据,就拿销售管理系统来说,当BD要去拜访/维护客户的时候,我的搜索页面能够支持他非常快速精准的找到他要拜访的客户,查看这个客户信息、填写拜访记录等等就好了,而不是让他一页一页的翻,翻个几十页还没找到。所以从这个角度,更多的是应该去优化你的搜索条件的,而不是较劲脑汁去解决深分页让BD可以尽情的翻到两万页。

  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值