bigywater.tech
Elasticsearch · 50

from + size 分页的深分页问题

真实灌5万条文档验证from+size深分页的默认硬性上限与性能衰减规律,并用search_after在同样深度下的真实耗时对比,证明游标式分页如何避开深分页越翻越慢的陷阱。

from=10000
真实被硬性拒绝,默认上限明确写在报错里
from=49990深分页
真实比第一页慢了15到28倍
search_after同样深度
真实稳定1ms,跟第一页速度完全一样

做的事:真实灌5万条文档,直接用from+size翻到超过默认上限的深度

代码真实索引50000条文档,每条带一个递增的n字段 POST /page_test/_search {"from":0,"size":10,"query":{"match_all":{}},"sort":[{"n":"asc"}]} POST /page_test/_search {"from":10000,"size":10,"query":{"match_all":{}}}

验证:from+size一旦超过10000,真实直接被ES拒绝,不是慢慢变慢而是硬性报错

实测 · 本机 Elasticsearch 8.15.0 执行输出
from=0 size=10(第一页): took=9ms, 1ms, 1ms

from=10000 size=10:
→ 400 illegal_argument_exception:
  "Result window is too large, from + size must be less than or
   equal to: [10000] but was [10010]. See the scroll api for a
   more efficient way to request large data sets."

这条真实报错证明了ES对深分页设置了一道硬性的默认上限:index.max_result_window默认值是10000,from+size一旦超过这个数字,查询真实直接被拒绝,而不是放任它执行然后在性能上慢慢变差。这个限制真实是ES主动的保护机制——它在报错里直接点名了真实推荐的替代方案(scroll API,以及后面会验证的search_after),说明官方很清楚深分页本身就是一个真实有性能隐患的操作模式,默认直接把路堵死,逼着使用者改用更合理的方案。

真实调大max_result_window后,对比第一页和接近末尾的深分页各自真实的耗时

代码PUT /page_test/_settings {"index.max_result_window":60000} POST /page_test/_search {"from":0,"size":10,"query":{"match_all":{}},"sort":[{"n":"asc"}]} POST /page_test/_search {"from":49990,"size":10,"query":{"match_all":{}},"sort":[{"n":"asc"}]}

验证:越往后翻页真实越慢,from=49990比from=0真实慢了15到28倍

实测 · 本机执行输出(各跑3次)
from=0 size=10(第一页): took=1ms, 1ms, 1ms

from=49990 size=10(翻到接近5万条数据的末尾): took=28ms, 19ms, 15ms
  ← 真实比第一页慢了15到28倍
这组真实耗时差异的根源在于from+size分页的底层实现方式:哪怕最终只想要10条结果,ES在每个分片上真实都要先完整地计算并排序出from+size条结果(本次实测就是49990+10=50000条,相当于这个分片里的全部文档),再把这一大批候选结果送回协调节点做合并,最后真实只截取用户想要的那10条返回。也就是说,from这个数字越大,真实需要排序和传输的候选结果就越多,from=49990这次查询背后真实做的排序和数据搬运工作量,跟直接要5万条结果几乎一样大,只是最后扔掉了前面49990条——这正是深分页"越往后翻越慢"的真实原因,而且这个代价会随着from继续增大而真实持续恶化,不是一次性的固定开销。

真实用search_after在同样的深度查询,验证它是否能避开这个性能陷阱

代码POST /page_test/_search {"size":10,"query":{"match_all":{}},"sort":[{"n":"asc"}],"search_after":[49989]}

验证:search_after在同样深度下真实稳定回到1ms,跟第一页的速度完全一样

实测 · 本机执行输出(各跑3次)
search_after从排序值49989继续往后翻10条: took=6ms, 1ms, 1ms
  ← 预热后真实稳定在1ms,跟from=0的第一页速度完全一样
  ← 跟同样深度的from=49990相比(15-28ms),真实快了15到28倍

这组真实结果验证了search_after为什么能真正解决深分页的性能问题:它不依赖"数到第N条"这种需要排序全部前置结果的方式,而是真实传入上一页最后一条记录的排序值(本次是n=49989)作为"游标",让ES直接利用排序字段的索引结构真实跳转到这个值之后的位置继续取结果,不需要像from+size那样真实把前面所有候选结果都排序一遍再丢弃。代价是search_after真实只能"往后翻一页接一页",不能像from+size那样真实随意跳转到任意页码——这也是为什么它更适合"无限滚动加载"这种场景,而不适合"直接跳到第500页"这种传统分页UI。

分步演示:from+size深分页从硬性上限到真实性能衰减,再到search_after的完整对比

每一步都是本文上面在本机真实的Elasticsearch 8.15.0上跑出来的原始结果。

from+size深分页真实越翻越慢,search_after用游标避开这个陷阱
这一步新验证的分页方式

    本文用到的工具

    • 本机真实部署的 Elasticsearch 8.15.0(单节点,tar包直接启动)
    • 真实的 from+size 分页与 search_after API
    京ICP备2026067502号-1京公网安备11010502063487号