Elasticsearch 深分页问题
Elasticsearch 深分页问题
1️⃣ 什么是深分页 (Deep Pagination)
在 Elasticsearch 中,我们可以通过 from + size
进行分页查询:
GET my_index/_search
{"from": 100000,"size": 10
}
这表示 跳过前 100000 条,取 10 条记录。
这种情况就属于 “深分页”,因为需要跨过几十万甚至上百万条数据后再取结果。
📌 官方定义:深分页是指 from + size
很大,尤其是 from
很大(如几十万、百万级)。
2️⃣ 为什么深分页有性能问题?
原因 1:ES 基于 Lucene,查询需要拉取全部数据再丢弃
- Lucene 不支持直接 O(1) 跳过前 N 条。
- 即使
from=100000
只需要 10 条,ES 仍需:- 从不同的 Shard 拉取 前 100010 条候选文档
- 按
score
(相关性)排序 - 丢掉前 100000 条,只保留最后 10 条
带来的影响:
- 内存消耗巨大
- CPU 排序开销大
- Shard 越多,代价越大(每个分片都要返回
from+size
条数据)
原因 2:分布式聚合 & 排序代价高
假设 5 个分片:
- 每个分片返回
from + size
条数据到协调节点(Coordinating Node) - 协调节点合并排序
- 丢掉前
from
条
深分页时,网络传输 & 合并瓶颈显著。
💡 结论:from
很大 → 查询性能急剧下降(甚至 OOM)
3️⃣ 官方建议和优化方案
✅ 方案 1:search_after
(推荐)
- 原理:基于上一页最后一条数据的排序值获取下一页
不用from
,避免重复扫描 - 缺点:不能直接跳到第 N 页,只能顺序翻页
- 要求:有唯一且稳定的排序字段(如:时间戳 +
_id
)
第一次请求:
GET my_index/_search
{"size": 10,"sort": [{ "timestamp": "asc" },{ "_id": "asc" }]
}
记住最后一条的 (timestamp, _id)
。
下一页请求:
GET my_index/_search
{"size": 10,"sort": [{ "timestamp": "asc" },{ "_id": "asc" }],"search_after": [ "2024-06-26T10:00:00Z", "abc123" ]
}
优点:
- 避免全量扫描
- 性能可控稳定
缺点:
- 只能顺序翻页,不能跳转任意页
✅ 方案 2:scroll API
(大批量导出)
- 用于批量数据导出(不是普通分页展示)
- 固定查询“快照”,不会随数据变更
- 持有游标的代价较高
首次查询:
POST my_index/_search?scroll=1m
{"size": 1000,"query": { "match_all": {} }
}
拉取下一批:
POST _search/scroll
{"scroll": "1m","scroll_id": "DxF...abc"
}
✅ 方案 3:Point In Time(PIT)
- ES 7.10+ 新增
- 类似
scroll
,但轻量,不固定数据快照 - 一般结合
search_after
使用
创建 PIT:
POST my_index/_pit?keep_alive=1m
✅ 方案 4:业务层优化
- 限制分页深度(例如最多翻到第 100 页)
- 改用时间范围 + 条件分页(
search_after
按时间+ID 排序更稳定) - 建立预聚合/索引表,提前规整数据
4️⃣ 方案对比
方法 | 跳任意页 | 性能 | 场景 |
---|---|---|---|
from+size | ✅ | 差 | 小数据量分页 |
search_after | ❌ | 好 | 顺序分页大数据 |
scroll | ❌ | 好 | 批量导出/全量遍历 |
PIT+search_after | ❌ | 好 | 实时数据流式分页 |
🎯 最佳实践
- 数据量小(<1w):直接 from+size
- 大数据 + 顺序翻页:
search_after
- 大批量导出:
scroll
- 实时数据深翻:PIT +
search_after