深分页为什么慢
偏移分页的共同问题是代价随页深增长:SQL 的 OFFSET 要让引擎逐行跳过,MongoDB 的 skip 同理,Elasticsearch 的 from+size 还受 index.max_result_window 限制且每页都要在协调节点归并。键集与游标分页改用上一页末位值作为下一页的起点条件,代价与页深基本无关,代价是不能随机跳到第 N 页,只能顺序前后翻。
基本信息
| offset 方式 | SQL:LIMIT n OFFSET m;MongoDB:skip(m).limit(n) |
|---|---|
| 键集方式 | WHERE (排序键) < 上一页末值 ORDER BY 排序键 LIMIT n |
| 游标方式 | MongoDB 用 _id 或复合键做游标;ES 用 search_after 配 PIT |
| 平台限制 | ES 的 from+size 默认上限 10000(index.max_result_window) |
| 前提条件 | 排序键必须能唯一确定顺序,通常用时间戳加唯一 ID |
典型使用场景
- 后台导出与对账:按游标整表顺序拉取,中途数据变化也不漏不乱
- 信息流与时间线:按时间倒序滑动加载,天然适合用末位时间戳续页
- 搜索结果深翻:ES 用 search_after 加 PIT 保证排序快照一致,绕开窗口上限
- 需要页码跳转的后台列表:只在偏移量小的范围内用 offset,超过阈值改按键集
- 增量同步:把上一轮的最大时间戳或 ID 存下来作为下一轮起点
键集分页示例
键集分页示例
-- 第一页:按时间倒序,带上唯一 ID 保证顺序稳定
SELECT id, ts, msg FROM events ORDER BY ts DESC, id DESC LIMIT 20;
-- 下一页:用上一页最后一条的 (ts, id) 作为起点游标
SELECT id, ts, msg FROM events
WHERE (ts, id) < ('2026-10-12 00:00:00', 1001)
ORDER BY ts DESC, id DESC LIMIT 20;
-- 复合游标要配复合索引,否则排序本身就要付出代价
-- CREATE INDEX idx_events_ts_id ON events (ts DESC, id DESC);ES 深分页(search_after)
ES 深分页(search_after)
curl -s 'http://es:9200/logs/_search' -H 'Content-Type: application/json' -d '{
"size": 20, "sort": [{"@timestamp": "desc"}, {"_id": "asc"}]
}'
# 把上一页最后一条的 sort 值填入 search_after,即得下一页
# 需要稳定快照时配合 PIT(point-in-time)使用,避免翻页期间刷新导致错行方式对照
| 方式 | 页深代价 | 适用 |
|---|---|---|
| offset | 随偏移量线性增长,深页极慢 | 小偏移、必须支持页码跳转 |
| 键集 | 与页深基本无关 | 顺序翻页、导出、增量拉取 |
| 游标 | 与页深基本无关,服务端维持状态 | 长列表滑动、深分页检索 |
| 随机跳页 | 只有 offset 支持 | 运营后台,需限制总页数或改用筛选条件 |
偏移分页在数据变动时会错行:新数据插到前面会让后一页漏掉或重复一条,只读快照场景影响不明显,边写边翻页的接口应优先用游标。
游标值来自客户端时要做校验:游标里包含的排序键需要能反序列化并校验范围,否则可被构造出全表扫描或越权读取。