Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • ES CRUD
  • ES分布式架构
  • ES ISR机制
  • ES深度分页查询优化
    • 一、深度分页:为什么会成为性能陷阱?
      • 1.1 什么是深度分页?
      • 1.2 from+size:浅分页的“甜蜜陷阱”
      • 1.3 分布式架构下的聚合陷阱
      • 1.4 官方的“保命机制”:maxresultwindow
    • 二、深度分页解决方案全解析
      • 方案一:from+size 浅分页(业务限制法)
      • 适用场景
      • 实现方式
      • 优缺点
      • 方案二:Scroll API(快照分页法)
      • 适用场景
      • 实现原理
      • 代码示例
      • 优缺点
      • 方案三:Search After(实时游标分页法)
      • 适用场景
      • 实现原理
      • 代码示例(基础版:仅往后翻页)
      • 进阶:实现往前翻页
      • 优缺点
    • 三、方案对比与选择建议
    • 四、总结
  • ES数据写入到查询
  • ELK的底层逻辑
  • ES ILM
  • 《Elasticsearch》笔记
Tavio
2025-06-11
目录

ES深度分页查询优化

在 Elasticsearch的日常应用中,分页查询是高频需求——无论是用户浏览商品列表、后台系统查看数据记录,还是数据分析中的批量提取,都离不开分页。但随着数据量增长,当用户翻到几十甚至上百页后,查询响应会急剧变慢,甚至出现集群崩溃风险。

# 一、深度分页:为什么会成为性能陷阱?

# 1.1 什么是深度分页?

通常我们将「页码较深的分页查询」称为深度分页。例如,用户查询第 100 页数据(from=990,size=10),或直接跳转到第 500 页,都属于深度分页场景。此时,ES 的查询性能会显著下降,甚至触发内置保护机制。

# 1.2 from+size:浅分页的“甜蜜陷阱”

日常开发中,我们习惯用 from+size 实现分页,语法简单直观:

{
  "query": { "match_all": {} },
  "from": 10,  // 跳过前10条
  "size": 20   // 返回20条
}
1
2
3
4
5

这种方式在浅分页(如前 10 页)或数据量较小时表现良好,但在深度分页场景下会暴露严重问题。

# 1.3 分布式架构下的聚合陷阱

ES 是分布式存储,索引数据分散在多个分片上。当使用 from+size 时,查询流程如下:

  1. 分片查询:每个分片需返回 from+size 条数据(而非 size 条)。例如 from=9000,size=20 时,每个分片需返回 9020 条数据;
  2. 协调节点聚合:协调节点收集所有分片的 9020 条数据,在内存中合并、排序,最终截取第 9000~9020 条返回。

随着 from 增大,每个分片返回的数据量剧增,导致:

  • 网络传输量暴增(如 3 个分片时,from=10w 需传输 3*(10w+size) 条数据);
  • 协调节点内存消耗飙升,排序耗时指数级增长;
  • 接口 P90 响应时间大幅延长(90% 的请求响应变慢)。

# 1.4 官方的“保命机制”:max_result_window

为避免深度分页拖垮集群,ES 默认设置 max_result_window=10000(可通过索引设置修改)。当 from+size 超过该值时,ES 会直接报错:

{
  "error": {
    "root_cause": [
      { "type": "query_phase_execution_exception", "reason": "Result window is too large..." }
    ]
  }
}
1
2
3
4
5
6
7

这意味着,即使你想“硬扛”深度分页,ES 也会主动限制。

# 二、深度分页解决方案全解析

针对深度分页问题,ES 提供了三种方案,各有适用场景。

# 方案一:from+size 浅分页(业务限制法)

核心思路:从业务层面限制分页深度,避免触发 max_result_window 和性能问题。

# 适用场景

  • 数据量较小(总数据量 < 10w);
  • 需支持跳页(如用户直接点击第 5 页);
  • 对实时性要求高(如最新数据查询)。

# 实现方式

  1. 保留 from+size 语法,但限制最大页码(如仅允许查询前 100 页);
  2. 前端展示时截断总条数(避免用户尝试翻到过深页码)。

代码示例:

// Java 后端:限制 from 不超过 990(即第 100 页,size=10)
int maxFrom = 990;
int from = (page - 1) * size;
if (from > maxFrom) {
    throw new RuntimeException("最多支持查询前 100 页");
}

SearchRequest esRequest = new SearchRequest(indexName);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery())
             .sort("id", SortOrder.ASC)
             .from(from)
             .size(size);
esRequest.source(sourceBuilder);

// 前端:截断总条数,避免显示过深页码
this.total = response.total > 1000 ? 1000 : response.total; 
// 假设 size=10,1000 对应 100 页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 优缺点

  • 优点:实现简单,支持跳页,实时性好;
  • 缺点:无法支持深度分页(如超过 100 页),依赖业务限制。

# 方案二:Scroll API(快照分页法)

核心思路:创建数据快照,通过 scroll_id 分批获取数据,避免重复计算。

# 适用场景

  • 批量数据导出(如导出 10w 条历史数据);
  • 全量数据遍历(如数据迁移、索引重建);
  • 不要求实时性,且无需跳页(仅支持顺序分页)。

# 实现原理

  1. 创建快照:首次查询时,ES 在所有分片上创建数据快照(查询时的静态数据,新增/修改的数据不会被包含),并返回 scroll_id;
  2. 分批获取:后续分页通过 scroll_id 从快照中获取下一页数据,scroll_id 有效期可设置(如 1 分钟);
  3. 清理资源:查询结束后需主动清理 scroll_id,避免占用集群内存。

# 代码示例

// 1. 初始化 Scroll(设置快照有效期为 1 分钟)
Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1));
SearchRequest request = new SearchRequest(indexName);
request.scroll(scroll);

// 2. 首次查询,生成 scroll_id
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery())
             .sort("id", SortOrder.ASC)
             .size(100); // 每次返回 100 条
request.source(sourceBuilder);

SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
String scrollId = response.getScrollId();
SearchHit[] hits = response.getHits().getHits();

// 3. 循环获取下一页
List<HKAddressDTO> result = new ArrayList<>();
while (hits != null && hits.length > 0) {
    // 处理当前页数据
    for (SearchHit hit : hits) {
        result.add(JSON.parseObject(hit.getSourceAsString(), HKAddressDTO.class));
    }
    
    // 用 scroll_id 获取下一页
    SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
    scrollRequest.scroll(scroll); // 重置有效期
    response = esClient.scroll(scrollRequest, RequestOptions.DEFAULT);
    scrollId = response.getScrollId();
    hits = response.getHits().getHits();
}

// 4. 清理 scroll_id(关键!避免内存泄漏)
ClearScrollRequest clearRequest = new ClearScrollRequest();
clearRequest.addScrollId(scrollId);
esClient.clearScroll(clearRequest, RequestOptions.DEFAULT);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 优缺点

  • 优点:支持海量数据分页,性能稳定(基于快照,无需重复计算);
  • 缺点:
    • 数据非实时(快照创建后的数据变更不生效);
    • 不支持跳页(只能顺序往后翻);
    • scroll_id 占用内存,需及时清理,否则可能导致节点宕机。

# 方案三:Search After(实时游标分页法)

核心思路:基于上一页最后一条数据的排序值(游标)定位下一页,彻底摆脱 from 的限制,是 ES 5+ 推荐的深度分页方案。

# 适用场景

  • 深度分页且需实时性(如用户无限滚动加载数据);
  • 仅需顺序分页(不支持跳页,但可实现前后翻页);
  • 数据量极大(超过 10w 甚至 100w)。

# 实现原理

  1. 排序字段要求:必须指定唯一且稳定的排序字段(如主键 id),避免排序冲突导致分页错乱;
  2. 游标定位:首次查询时,记录最后一条数据的 sort 值(游标);下一页查询时,通过 search_after 参数传入该游标,ES 直接从游标位置往后查询;
  3. 分布式协调:每个分片独立维护有序数据,协调节点将 search_after 条件下发到分片,分片仅返回排序值大于游标的前 size 条数据,大幅减少传输和计算量。

# 代码示例(基础版:仅往后翻页)

List<HKAddressDTO> queryBySearchAfter(Object[] lastSortValue, int size) {
    SearchRequest request = new SearchRequest(indexName);
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    
    // 1. 构建查询(必须指定唯一排序字段)
    sourceBuilder.query(QueryBuilders.matchAllQuery())
                 .sort("id", SortOrder.ASC); // 以 id 正序排序
    
    // 2. 非首次查询时,设置上一页的最后一个游标
    if (lastSortValue != null && lastSortValue.length > 0) {
        sourceBuilder.searchAfter(lastSortValue);
    }
    
    // 3. 设置每页大小
    sourceBuilder.size(size);
    request.source(sourceBuilder);
    
    // 4. 执行查询并处理结果
    SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
    SearchHit[] hits = response.getHits().getHits();
    List<HKAddressDTO> result = new ArrayList<>();
    Object[] currentLastSortValue = null;
    
    for (SearchHit hit : hits) {
        result.add(JSON.parseObject(hit.getSourceAsString(), HKAddressDTO.class));
        currentLastSortValue = hit.getSortValues(); // 更新当前页最后一个游标
    }
    
    // 返回数据和当前页最后一个游标(供下一页使用)
    return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 进阶:实现往前翻页

Search After 本身不支持跳页,但可通过“保存首尾游标 + 反向排序”实现往前翻页:

  1. 保存游标:每次查询时,记录当前页第一条和最后一条数据的 sort 值(如 firstSort 和 lastSort);
  2. 往前翻页:当用户点击“上一页”时,以 firstSort 为游标,使用反向排序查询 size 条数据。

示例:
当前页数据排序值为 [20, 21, ..., 30](正序),往前翻页时:

// 反向排序(id 倒序),以当前页第一条的 sort 值为游标
sourceBuilder.query(QueryBuilders.matchAllQuery())
             .sort("id", SortOrder.DESC) // 倒序
             .searchAfter(firstSortValue); // firstSortValue = 20
1
2
3
4

查询结果为 [19, 18, ..., 10],反转后即为上一页数据 [10, 11, ..., 19]。

# 优缺点

  • 优点:
    • 无 max_result_window 限制,支持无限深度分页;
    • 基于实时数据(非快照),数据最新;
    • 性能优异(仅传输当前页所需数据);
  • 缺点:
    • 不支持直接跳页(需顺序翻);
    • 依赖排序字段的唯一性和稳定性;
    • 往前翻页需额外开发逻辑(保存首尾游标)。

# 三、方案对比与选择建议

方案 适用场景 支持跳页 实时性 性能(深度分页) 限制
from+size 浅分页、需跳页 是 实时 差(随深度下降) 受 max_result_window 限制
Scroll API 批量导出、全量遍历 否 快照 优 占用内存,需清理 scroll_id
Search After 深度分页、实时性要求高 否(可顺序翻) 实时 优 依赖唯一排序字段

选择建议:

  • 后台管理系统(需跳页,数据量小)→ from+size + 页码限制;
  • 数据导出/全量同步 → Scroll API;
  • 移动端无限滚动、海量数据分页 → Search After。

# 四、总结

ES 深度分页的核心矛盾是分布式架构下的聚合成本。from+size 适合简单场景但受限于深度,Scroll API 适合批量处理但牺牲实时性,Search After 则是平衡实时性和深度分页的最优解。

编辑 (opens new window)
#ES 深度分页查询
上次更新: 2026/01/21, 19:29:14
ES ISR机制
ES数据写入到查询

← ES ISR机制 ES数据写入到查询→

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式