当前位置:网站首页>浅谈深分页问题
浅谈深分页问题
2022-07-28 05:30:00 【georgesnoopy】
曾经面试被问到怎么解决深分页问题,因为以前项目用来es,所以重点问的是es的深分页问题,问我怎么解决深分页的问题,我的回答就是没解决。因为我觉得深分页就解决不了(先别浮现出各种网上解决深分页的方式,然后反驳这句话)
与其讨论什么从技术上去解决,还不如换个解读想下:
- 什么样的场景下会触发深分页?
- 当真的出现深分页问题的时候,如何实现系统的自我保护?
对第一个问题:我觉得就两种情况:第一:正常的业务流量,用户真的闲得蛋疼了,真的就一页一页的往下翻,翻到了几千上万页(只有闲得蛋疼的才会去做这件事情,否则根本没有时间和耐心)。第二:非正常业务流量,被爬了之类的。
那么针对这两种场景要怎么解决,我的答案还是不解决,我需要做的就是真的出现了做好系统保护就好,比如当分页到了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可以尽情的翻到两万页。
边栏推荐
- Record the first article of so and so Xiao Lu
- Codesensor: convert the code into AST and then into text vector
- 爬虫学习总结
- Log in to Oracle10g OEM and want to manage the monitor program, but the account password input page always pops up
- freemarker导出word,带表格和多张图片,解决图片重复和变形
- 多进程(多核运算)Multiprocessing
- Erudite Valley Learning Records] Super summary, attentive sharing | common APIs
- Standard C language learning summary 7
- Small turtle C (Chapter 5 loop control structure program 567) break and continue statements
- Shell--第一天作业
猜你喜欢

Serial port configuration of raspberry pie

MySQL excludes holidays and calculates the date difference

Softmax multi classification gradient derivation

MOOC Weng Kai C language fourth week: further judgment and circulation: 3. Multiple branches 4. Examples of circulation 5. Common errors in judgment and circulation

Easypoi export interlaced style settings

vcf文件制作

WiFi one click connection configuration of raspberry pie

Understanding of maximum likelihood estimation, gradient descent, linear regression and logistic regression

Svg understanding and drawing application

NAT-网络地址转换
随机推荐
ESLint常见问题解决方案集锦
Branch and loop statements
Easypoi export table with echars chart
Construction of Yum warehouse
easypoi一对多,合并单元格,并且根据内容自适应行高
Softmax multi classification gradient derivation
Insert sort of sort
Standard C language summary 2
Implementation method of converting ast into word vector before converting word vector
C language review (modifier article)
Pictures are adaptive to the screen
2018-cvpr-Gesture Recognition: Focus on the Hands
C language address book system
SySeVR环境配置:joern-0.3.1、Neo4j-2.1.5、py2neo2.0
How did tensor leak your memory / video memory
easypoi导出表格带echars图表
Sysevr environment configuration: joern-0.3.1, neo4j-2.1.5, py2neo2.0
Canvas drawing 1
Standard C language summary 4
TOPK problem