从一个分页问题开始
做分页查询,当分页达到一定量的时候,报如下错误
Result window is too large, from + size must be less than or equal to: [10000] but was [78020]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting.
解决方案
- 在业务中限制分页大小,使
from+size<=10000
; - 动态更改索引设置,为
max_result_window
参数赋值足够大的值。
PUT index/_settings
{
"index":{
"max_result_window":50000
}
}
推荐使用第一种解决方案,因为es的优势在于搜索,不在于分页,对此做限制就是为不影响其性。就es的默认配置,假设每页10条记录,也有1000页,如果业务上实在不妥协,则使用第二种方案。
溯源
以上解决方案均是基于max_result_window=10000来进行展开的。那么为什么ES官方会有此参数的限制呢?
max_result_window的值可以无限制修改吗?接下来做个简单示例:
场景复现
新建测试库(PUT请求)
localhost:9200/chenjun_test_01/_bulk?refresh
{ "index" : { "_id" : 1 } }
{ "name" : "百里守约","type":1}
{ "index" : { "_id" : 2 } }
{ "name" : "凯","type":2}
{ "index" : { "_id" : 3 } }
{ "name" : "盾山", "type":3}
查询数据(GET请求)
http://127.0.0.1:9200/chenjun_test_01/_search
返回值:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "1",
"_score": 1.0,
"_source": {
"name": "百里守约",
"type": 1
}
},
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "2",
"_score": 1.0,
"_source": {
"name": "凯",
"type": 2
}
},
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "3",
"_score": 1.0,
"_source": {
"name": "盾山",
"type": 3
}
}
]
}
}
模拟分页查询(正常查询)(GET请求):
http://127.0.0.1:9200/chenjun_test_01/_search
{
"from": 0,
"size": 1,
"query": {
"match_all": {
}
}
}
返回值:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "1",
"_score": 1.0,
"_source": {
"name": "百里守约",
"type": 1
}
}
]
}
}
测试临界值(GET请求):
http://127.0.0.1:9200/chenjun_test_01/_search
{
"from": 9990, //模拟第999页
"size": 10,// 查询10条
"query": {
"match_all": {
}
}
}
返回结果:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 1.0,
"hits": []
}
}
http://127.0.0.1:9200/chenjun_test_01/_search
{
"from": 10000, // 测试第1000页
"size": 1,// 查询1条
"query": {
"match_all": {
}
}
}
返回结果:
{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}
],
"type": "search_phase_execution_exception",
"reason": "all shards failed",
"phase": "query",
"grouped": true,
"failed_shards": [
{
"shard": 0,
"index": "chenjun_test_01",
"node": "UuMcBk37TNWHjY4hVtzyVA",
"reason": {
"type": "illegal_argument_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}
}
],
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting.",
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}
}
},
"status": 400
}
此时,错误已经出现了,想必你有疑问,ES查一条数据都扛不住?不是号称海量数据引擎吗?先别急,这个我在后面再分析。
那么我们来验证解决方案的第二条,修改max_result_window的值,改成2万做个演示:
先检查下测试索引配置(GET请求):
http://127.0.0.1:9200/chenjun_test_01/_settings
返回值:
{
"chenjun_test_01": {
"settings": {
"index": {
"creation_date": "1673579058293",
"number_of_shards": "1",
"number_of_replicas": "1",
"uuid": "6F-jxX9VRCimqov4wR-eqg",
"version": {
"created": "7080199"
},
"provided_name": "chenjun_test_01"
}
}
}
}
修改配置(PUT请求)
http://127.0.0.1:9200/chenjun_test_01/_settings
{
"index":{
"max_result_window":20000
}
}
返回值:
{
"acknowledged": true
}
再查看下索引库配置信息(GET请求):
http://127.0.0.1:9200/chenjun_test_01/_settings
{
"chenjun_test_01": {
"settings": {
"index": {
"number_of_shards": "1",
"provided_name": "chenjun_test_01",
"max_result_window": "20000",
"creation_date": "1673579058293",
"number_of_replicas": "1",
"uuid": "6F-jxX9VRCimqov4wR-eqg",
"version": {
"created": "7080199"
}
}
}
}
}
可以看到,配置已经生效, "max_result_window": "20000"。
此时我们再去执行以下刚才的模拟分页查询(GET请求):
http://127.0.0.1:9200/chenjun_test_01/_search
返回值:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 1.0,
"hits": []
}
}
可以看到,查询确实不报错了。由此可见,修改max_result_window确实可以解决当前问题,但是,解决的也仅仅是当前的问题。当我们在生成环境中,随着业务增长,数据量增加,这个错误还会复现。
再次模拟分页,这次模拟从2000页开始,也只查询一条数据(GET请求)
http://127.0.0.1:9200/chenjun_test_01/_search
{
"from": 20000,
"size": 1,
"query": {
"match_all": {
}
}
}
返回值:
{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [20000] but was [20001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}
],
"type": "search_phase_execution_exception",
"reason": "all shards failed",
"phase": "query",
"grouped": true,
"failed_shards": [
{
"shard": 0,
"index": "chenjun_test_01",
"node": "UuMcBk37TNWHjY4hVtzyVA",
"reason": {
"type": "illegal_argument_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [20000] but was [20001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}
}
],
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [20000] but was [20001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting.",
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [20000] but was [20001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}
}
},
"status": 400
}
看到了吧,也就是说一旦数据达到max_result_window窗口设的值,es就不给你查询了。现象已然清楚,那么让我们回到最初,为什么ES官方会有此参数的限制呢?
问题分析
max_result_window是分页返回的最大数值,因为ES是分布式的,数据散落在各个节点,当你查询分页数据的时候,比如每页500条数据,那么需要在每个Shard先进行排序,取前500条数据,最后把每个Shard的数据在JVM中汇总,再次排序,最后取前500。max_result_window本身是对JVM的一种保护,通过设定一个合理的阈值,避免初学者分页查询时由于单页数据过而导致OOM。很多人会告诉你如果想要增加单页数据的上限,放开这个参数就行,但是如果你不知道这个参数的意义,那么就会导致频分的内存溢出而且很难找到原因,设置一个合理的大小是需要通过你的各项参数来确定的,比如你用户量、数据量、物理内存的大小等等,通过监控数据和分析各项指标从而确定一个最佳值,并非越大约好,JVM不能超过物理内存的一半。
演示的时候,我特意模拟1000页开始和2000页开始只查一条数据,导致es查询崩盘的案例,可能这个对我们有疑问,ES不是号称海量数据杀手吗?怎么才几万数据查个一条就扛不住了,海量在哪?
这个就值得讨论一波的,es的海量体现在搜索上,而不是做分页操作上,Elasticsearch 是位于 Elastic Stack 核心的分布式搜索和分析引擎,它是搜索引擎,依靠的是其内部的倒排索引机制,仅仅是对海量数据的全文匹配来说,它是海量数据杀手的依据。而我们现在遇到的是深度分页问题,自然无法体现它的优势。
来看一下为什么我们深分页后,查一条数据为什么都不给查,说这个得拿es分布式存储系统中分页查询的过程来举例,假设在一个有 4 个主分片的索引中搜索,每页返回10条记录。当我们请求结果的第1页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 40 个结果排序得到全部结果的前 10 个。当我们请求第 99 页(结果从 990 到 1000),需要从每个分片中获取满足查询条件的前1000个结果,返回给协调节点, 然后协调节点对全部 4000 个结果排序,获取前10个记录。当请求第10000页,每页10条记录,则需要先从每个分片中获取满足查询条件的前100010个结果,返回给协调节点。然后协调节点需要对全部(100010 * 分片数4)的结果进行排序,然后返回前10个记录。可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。
以上测试的例子中,表面上你分页从10000条开始,只查一条,实际上es需要查询from+size=10001条数据,然后再将那个1过滤给你。在数据量非常大的情况下,from+size分页会把全部记录加载到内存中,这样做不但运行速递特别慢,而且容易让es出现内存不足而挂掉。比如要取第1000页的数据,在分页的时候,es需要首先在每一个节点上取出10001的数据,然后对数据进行排序,取出排序后在10000到10001的数据,然后返回。这样随着数据量的增大,每次分页时排序的开销会越来越大。这就是 ES搜索引擎对任何查询都不要返回超过 10000 个结果的设的的原因。
分页方案
ES支持的三种分页查询方式
- From + Size 查询
- Scroll 遍历查询
- Search After 查询
From + Size
es 默认采用的分页方式是 from+ size 的形式。
默认返回前10个匹配的匹配项。其中:
from:未指定,默认值是 0,注意不是1,代表当前页返回数据的起始值。
size:未指定,默认值是 10,代表当前页返回数据的条数。
优缺点
但是这种分页方式,随着深度分页的递进,对内存和查询效率是不友好的,在深度分页的情况下,这种使用方式效率是非常低的,除了效率上的问题,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window,默认为 10000 。也就是当 from + size > max_result_window 时,es 将返回错误。
官方建议:
1、避免过度使用 from 和 size 来分页或一次请求太多结果。
2、不推荐使用 from + size 做深度分页查询的核心原因:
搜索请求通常跨越多个分片,每个分片必须将其请求的命中内容以及任何先前页面的命中内容加载到内存中。
对于翻页较深的页面或大量结果,这些操作会显著增加内存和 CPU 使用率,从而导致性能下降或节点故障。
Scroll
有一种查询场景,我们需要一次性或者每次查询大量的文档,但是对实时性要求并不高。ES针对这种场景提供了scroll api的方案。这个方案牺牲了实时性,但是查询效率确实非常高。不要把 scroll用于实时请求,它主要用于大数据量的场景。例如:将一个索引的内容索引到另一个不同配置的新索引中。
测试查询(GET请求)
在请求后面追加scroll=1m参数,表示1分钟内有效
http://127.0.0.1:9200/chenjun_test_01/_search?scroll=1m
{
"size": 3, // 设置每页3条数据进行分页滚动查询
"query": {
"match_all" : {
}
}
}
返回结果:
{
"_scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFC05X1hxWVVCVUNTejNTcmFVMW5KAAAAAAAAAF0WVXVNY0JrMzdUTldIalk0aFZ0enlWQQ==",
"took": 5,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 23,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "1",
"_score": 1.0,
"_source": {
"name": "百里守约",
"type": 1
}
},
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "2",
"_score": 1.0,
"_source": {
"name": "凯",
"type": 2
}
},
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "3",
"_score": 1.0,
"_source": {
"name": "盾山",
"type": 3
}
}
]
}
}
接下来继续查询,我们带上上一次查询结果返回的_scroll_id作为查询参数:
// 此时,可以不用指定索引库和查询条件了,直接使用scroll_id进行查询,1分钟内均有效
http://127.0.0.1:9200/_search/scroll
{
"scroll": "1m",
"scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFC05X1hxWVVCVUNTejNTcmFVMW5KAAAAAAAAAF0WVXVNY0JrMzdUTldIalk0aFZ0enlWQQ=="
}
返回值
{
"_scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFC05X1hxWVVCVUNTejNTcmFVMW5KAAAAAAAAAF0WVXVNY0JrMzdUTldIalk0aFZ0enlWQQ==",
"took": 20,
"timed_out": false,
"terminated_early": true,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 23,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "4",
"_score": 1.0,
"_source": {
"name": "东皇太一",
"type": 1
}
},
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "5",
"_score": 1.0,
"_source": {
"name": "孙悟空",
"type": 2
}
},
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "6",
"_score": 1.0,
"_source": {
"name": "后裔",
"type": 3
}
}
]
}
}
可以看到获取到了第二页的数据,获得的结果里面包含有一个scoll_id,下一次再发送scoll请求的时候,必须带上这个scoll_id,接下来获取第三页数据。以此类推,后面每次滚屏都把前一个的scroll_id复制过来。注意到,后续请求时没有了index信息,size信息等,这些都在初始请求中,只需要使用scroll_id和scroll两个参数即可。
很多人对scroll这个参数容易混淆,误认为是查询的限制时间。这个理解是错误的。这个时间其实指的是es把本次快照的结果缓存起来的有效时间。scroll 参数相当于告诉了 ES我们的search context要保持多久,后面每个 scroll 请求都会设置一个新的过期时间,以确保我们可以一直进行下一页操作。
scroll这种方式为什么会比较高效
ES的检索分为查询(query)和获取(fetch)两个阶段,query阶段比较高效,只是查询满足条件的文档id汇总起来。fetch阶段则基于每个分片的结果在coordinating节点上进行全局排序,然后最终计算出结果。scroll查询的时候,在query阶段把符合条件的文档id保存在前面提到的search context里。 后面每次scroll分批取回只是根据scroll_id定位到游标的位置,然后抓取size大小的结果集即可,少了排序计算的过程,故而比较高效。
Scroll API 原理上是对某次查询生成一个游标 scroll_id , 后续的查询只需要根据这个游标去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。scroll_id 的生成可以理解为建立了一个临时的历史快照,在此之后的增删改查等操作不会影响到这个快照的结果。
所有文档获取完毕之后,需要手动清理掉 scroll_id 。虽然es 会有自动清理机制,但是 srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。所以用完之后要及时清理。使用 es 提供的 CLEAR_API 来删除指定的 scroll_id
删除视图快照(DELETE请求)
http://127.0.0.1:9200/_search/scroll
{
"scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFC05X1hxWVVCVUNTejNTcmFVMW5KAAAAAAAAAF0WVXVNY0JrMzdUTldIalk0aFZ0enlWQQ=="
}
优缺点
scroll查询的相应数据是非实时的,scoll滚动搜索技术,一批一批查询。scoll搜索会在第一次搜索的时候,保存一个当时的视图快照,之后只会基于该旧的视图快照提供数据搜索,如果这个期间数据变更,是查询不到的,并且保留视图快照需要足够的堆内存空间。
官方文档强调:不再建议使用scroll API进行深度分页。如果要分页检索超过 Top 10,000+ 结果时,推荐使用:PIT + search_after。
适用场景
全量或数据量很大时遍历结果数据,而非分页查询。
search after
为社么会有 search_after?
官方的建议scroll 并不适用于实时的请求,因为每一个 scroll_id 不仅会占用大量的资源(特别是排序的请求),而且是生成的历史快照,对于数据的变更不会反映到快照上。这种方式往往用于非实时处理大量数据的情况,比如要进行数据迁移或者索引变更之类的。
那么在实时情况下如果处理深度分页的问题呢?
es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。search after利用实时有游标来帮我们解决实时滚动的问题,简单来说前一次查询的结果会返回一个唯一的字符串,下次查询带上这个字符串,进行`下一页`的查询,一看觉得和Scroll差不多,search_after有点类似scroll,但是和scroll又不一样,它提供一个活动的游标,通过上一次查询最后一条数据来进行下一次查询。
基本思想:searchAfter的方式通过维护一个实时游标来避免scroll的缺点,它可以用于实时请求和高并发场景。
看下官方说明:
测试一下:
http://127.0.0.1:9200/chenjun_test_01/_search
{
"size": 2,
"query": {
"match": {
"type": "3"
}
},
"sort": [
{
"_id": "desc" // 由于我创建的索引库字段较少,没有唯一标识一条记录的字段,就按id来吧
}
]
}
返回值:
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 8,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "9",
"_score": null,
"_source": {
"name": "小乔",
"type": 3
},
"sort": [
"9"
]
},
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "6",
"_score": null,
"_source": {
"name": "后裔",
"type": 3
},
"sort": [
"6" // 注意这个老六,一会要用它
]
}
]
}
}
上面的请求会为每一个文档返回一个包含sort排序值的数组。这些sort排序值可以被用于 search_after 参数里以便抓取下一页的数据。比如,我们可以使用最后的一个文档的sort排序值,将它传递给 search_after 参数:
http://127.0.0.1:9200/chenjun_test_01/_search
{
"size": 2,
"query": {
"match": {
"type": "3"
}
},
"search_after": [6], //这个值与上次查询最后一条数据的sort值一致,支持多个
"sort": [
{
"_id": "desc"
}
]
}
{
"took": 6,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 8,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "3",
"_score": null,
"_source": {
"name": "盾山",
"type": 3
},
"sort": [
"3"
]
},
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "22",
"_score": null,
"_source": {
"name": "宫本武藏",
"type": 3
},
"sort": [
"22"
]
}
]
}
}
官方提示:当我们使用search_after时,from值必须设置为0或者-1。
验证,以上查询条件我没写from,只写了size:2,默认from就是从0开始的。
http://127.0.0.1:9200/chenjun_test_01/_search
{
"from": 1,
"size": 2,
"query": {
"match": {
"type": "3"
}
},
"sort": [
{
"_id": "desc"
}
]
}
返回:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 8,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "6",
"_score": null,
"_source": {
"name": "后裔",
"type": 3
},
"sort": [
"6"
]
},
{
"_index": "chenjun_test_01",
"_type": "_doc",
"_id": "3",
"_score": null,
"_source": {
"name": "盾山",
"type": 3
},
"sort": [
"3"
]
}
]
}
}
=========================================
上面第一次查询设置了从1开始,然后查2个,没问题,我们也能拿到上次最后一个数据的sort,即盾山的sort,接下来
我们按search_after调用规则来继续携带这个追加参数"search_after": ["3"]来请求。
http://127.0.0.1:9200/chenjun_test_01/_search
{"from":1,
"size": 2,
"query": {
"match": {
"type": "3"
}
},
"search_after": ["3"],
"sort": [
{
"_id": "desc"
}
]
}
返回结果:
{
"error": {
"root_cause": [
{
"type": "search_exception",
"reason": "`from` parameter must be set to 0 when `search_after` is used."
}
],
"type": "search_phase_execution_exception",
"reason": "all shards failed",
"phase": "query",
"grouped": true,
"failed_shards": [
{
"shard": 0,
"index": "chenjun_test_01",
"node": "UuMcBk37TNWHjY4hVtzyVA",
"reason": {
"type": "search_exception",
"reason": "`from` parameter must be set to 0 when `search_after` is used."
}
}
]
},
"status": 500
}
Search_after不是自由跳转到随机页面的解决方案,而是并行滚动许多查询。它与scroll API非常相似,但与之不同的是,search_after参数是无状态的,它总是针对最新版本的搜索器进行解析。因此,排序顺序可能会在遍历过程中根据索引的更新和删除而改变。
优缺点
很明显,Search_after的缺点就是不能自由跳页,search_after 查询仅支持向后翻页。不严格受制于 max_result_window,可以无限制往后翻页,单次请求值不能超过 max_result_window,但总翻页结果集可以超过,那自然就无法应用到业务中的分页查询了。
总结
分布式存储引擎的深度分页目前没有完美的解决方案。
比如针对百度、google这种全文检索的查询,通过From+ size返回Top 10000 条数据完全能满足使用需求,末尾查询评分非常低的结果一般参考意义都不大。
From+ size:需要随机跳转不同分页(类似主流搜索引擎)、Top 10000 条数据之内分页显示场景。
search_after:仅需要向后翻页的场景及超过Top 10000 数据需要分页场景。
Scroll:需要遍历全量数据场景 。