MongoDB深分页(如skip 100万)性能差,因skip需线性扫描丢弃前N条,无法索引跳转;应改用游标分页,基于\_id或createdAt等有序字段+复合索引实现高效分页。

skip必须线性扫描前N条,无法利用索引跳转,性能必然崩。
为什么 skip + limit 在大数据量下会变慢
skip 不是“定位到第N条”,而是“逐条计数、丢弃前N条”。哪怕你加了{_id: 1}索引,MongoDB仍要从第一条开始遍历、匹配、计数,直到跳过指定数量。数据越往后,CPU和I/O消耗越线性增长。
- 执行
db.coll.find().sort({_id: 1}).skip(1000000).limit(20)时,实际扫描并丢弃了至少100万+条文档 - 如果查询还有其他条件(如
{status: "done"}),则需先过滤再skip,扫描量可能远超100万 - 分片集群下,
skip在每个shard上独立执行,mongos还要合并、二次排序,放大开销
用 _id 或时间字段做游标分页(推荐方案)
核心思路:不跳,只查“比上一页最后一条更大的下一批”。前提是排序字段有高效索引,且值唯一或基本有序(如 _id、createdAt)。
- 第一页:
db.coll.find({}).sort({_id: 1}).limit(50),拿到结果里最后一个_id(记为last_id) - 第二页起:
db.coll.find({_id: {$gt: last_id}}).sort({_id: 1}).limit(50) - 必须确保
_id字段上有索引(默认就有),否则$gt查询也会慢 - 若按时间分页,用
createdAt替代_id,但要注意同一秒内多条记录时,需复合索引{createdAt: 1, _id: 1}避免漏数据
复合查询 + 游标分页的写法陷阱
当分页带业务条件(如 {type: "order", status: "paid"})时,游标字段不能脱离查询上下文单独使用。
- 错误写法:
db.coll.find({type: "order"}).sort({createdAt: 1}).skip(10000).limit(20)—— 依然慢 - 正确写法:
db.coll.find({type: "order", status: "paid", createdAt: {$gt: last_time}}).sort({createdAt: 1, _id: 1}).limit(20) - 对应索引必须覆盖所有查询字段和排序字段:
db.coll.createIndex({type: 1, status: 1, createdAt: 1, _id: 1}) - 注意:如果
createdAt有重复,仅靠它做游标可能跳过或重复某几条,必须用复合排序 + 复合索引兜底
explain("executionStats") 是你唯一的验证依据
别猜,直接看执行计划。深分页优化是否生效,只看两个指标:
-
totalDocsExamined应该 ≈nReturned(比如取20条,扫描20–50条),而不是动辄几十万 -
executionStages.stage必须是IXSCAN,不能出现COLLSCAN或大量SORT - 运行
db.coll.find({...}).sort({...}).limit(...).explain("executionStats"),重点盯executionStats部分 - 如果
totalKeysExamined远大于nReturned,说明索引没被有效利用,得调索引字段顺序
skip 原点。


















