首页/数据/NoSQL 速查/分页策略对照:offset 与游标
选型与架构50 条

分页策略对照:offset 与游标

深分页的共同问题是 offset 越大代价越高:SQL OFFSET 要逐行跳过,MongoDB skip 类似,ES 的 from+size 受 index.max_result_window 限制。键集/游标分页用上一页末位值作为下一页条件,代价与页深基本无关,代价是不能随机跳页。

NoSQL 速查数据更新于 2026-10-13

深分页为什么慢

偏移分页的共同问题是代价随页深增长: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 支持运营后台,需限制总页数或改用筛选条件
偏移分页在数据变动时会错行:新数据插到前面会让后一页漏掉或重复一条,只读快照场景影响不明显,边写边翻页的接口应优先用游标。
游标值来自客户端时要做校验:游标里包含的排序键需要能反序列化并校验范围,否则可被构造出全表扫描或越权读取。

易错点

  • 用 offset 做后台全量导出:页数越多每页越慢,中途数据变化还会漏行重复,导出应使用游标加时间快照
  • 排序键不唯一就做键集分页:同一时间戳多条记录时边界处会稳定漏掉部分行,必须补唯一列兜底
  • 键集分页没有配套复合索引:每页仍要排序全量数据,省下了跳过行数却付出排序代价
  • 把 ES 的 from 提到几万:超出 max_result_window 直接报错,调大该参数又会显著增加堆压力,应改用 search_after

数据来源与参考

  • Elasticsearch Guide: Paginate search results
  • MongoDB Manual: cursor.skip()
  • PostgreSQL Docs: SELECT 中的 LIMIT 与 OFFSET
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌