游标分页($gt + _id)比skip()快一个数量级且性能不退化,因skip()需从头扫描前N条文档,而游标直接定位起始位置;百万级数据下skip()耗时随页码线性增长,游标则稳定高效。
直接结论:用+(或时间戳字段)做游标分页,比快一个数量级,且不随页码增长而退化。
为什么
在百万级数据后会越来越慢
MongoDB 的
不是“跳过索引位置”,而是让游标从头开始遍历、逐条计数跳过。即使有索引,它仍要扫描前 N 条匹配结果——N 越大,CPU 和 I/O 开销越明显。官方文档明确指出:
requires the server to scan from the beginning of the input results set。
10 万数据查第 1000 页(
):约 11 秒
同数据量用游标法(
):约 0.89 秒
跳过 86 万条记录时,
耗时飙升至 37 秒以上
如何实现 Search After 模式(即游标分页)
核心是把“第几页”转换成“从哪条之后取”。需满足两个前提:排序字段唯一、有序(推荐
或带索引的
)。
第一步:确保排序字段有索引,例如
或复用默认
索引
第二步:首次查询不带游标,但必须按排序字段升序/降序固定,例如
第三步:取响应中最后一条文档的
值(如
),作为下一页的起点
第四步:后续请求改用范围查询:
注意:升序用
,降序用
;若排序字段非唯一(如
),需加复合条件防漏/重,例如
。
MongoDB For Windows v3.5.4
MongoDB For Windows v3.5.4
下载
实际使用中容易踩的坑
游标分页不是“开箱即用”,几个关键细节决定是否真正生效:
前端必须保存并透传上一页最后一条的
(或复合游标值),不能只传页码;API 接口设计应接受
或
参数,而非
和
如果业务要求支持“跳转任意页码”,游标法无法直接满足——此时可折中:对前几百页仍用
(加缓存),靠后的页强制引导为“加载更多”模式
删除或插入会影响游标连续性,但不影响正确性;只是可能导致某页少 1 条或多 1 条(因新插入文档可能挤进当前游标区间),这是最终一致性下的合理代价
聚合管道中用
同样低效,应避免;如需在聚合后分页,先
+
,再用外部游标控制
什么时候还不得不回到
不是所有场景都适合游标分页。以下情况需谨慎评估:
需要精确总数(
在大数据集上本身也慢,且与游标分页逻辑冲突)
排序字段无索引或高重复率(如按状态字段分页),会导致范围查询返回不稳定条数
前端 UI 强依赖页码输入框(如后台管理列表),且无法改造交互逻辑
集合启用了分片,且
与分页排序字段不一致,mongos 合并结果时仍可能触发全局 skip
真正难处理的,从来不是“怎么写游标查询”,而是怎么让前后端协作把游标值可靠传递、持久化、失效清理——这部分往往比数据库调优更花时间。
$gt_idskip()skip()skip()cursor.skip()skip(9990)_id > ObjectId("...")skip()_idcreatedAtdb.users.createIndex({createdAt: 1})_iddb.users.find().sort({_id: 1}).limit(20)_idObjectId("65a1b2c3d4e5f67890123456")db.users.find({_id: {$gt: ObjectId("65a1b2c3d4e5f67890123456")}}).sort({_id: 1}).limit(20)$gt$ltcreatedAt{createdAt: {$gt: ISODate("...")}, _id: {$gt: ObjectId("...")}}_idlastIdcursorpagesizeskip()$skip$sort$limitskip()countDocuments()shard key