mongodb的skip()超10万变慢是因为存储引擎需从头线性扫描、逐条计数并丢弃前n条,索引无法跳过该过程;分片集群中各shard重复执行skip再合并,进一步放大i/o与cpu开销。

因为 skip() 不是跳过内存里的前 N 条,而是让存储引擎从头线性扫描、逐条计数并丢弃——哪怕你只想要第 100001 条。索引能加速匹配,但无法跳过计数过程。
为什么 skip() 超过 10 万就明显变慢
MongoDB 的 skip(N) 必须遍历排序后结果集的前 N 条文档,每条都要解码、校验、计数。即使有 {createdAt: -1, _id: -1} 复合索引,它仍要推进到第 N+1 个索引项才开始返回数据。文档体积越大、查询条件越宽泛(比如只按 status 过滤),实际扫描的索引节点和文档数量可能远超 N。
- 在分片集群中更糟:每个 shard 都得各自执行
skip(N),再由mongos合并;若某 shard 实际只匹配 5 条,它仍要扫描几千条来“凑够” skip 数量 - 副本集上主节点执行完再同步,延迟被放大
- 返回空结果时,
skip依然完整执行,纯属 I/O 和 CPU 浪费
用 find() + 范围条件替代 skip() 的实操要点
核心是放弃“第 N 页”思维,改用“从某条之后取下一页”。前提是排序字段具备足够区分度——单靠 createdAt 容易因时间重复导致漏/重,必须组合 _id 消除歧义。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
- 索引必须建为
{createdAt: -1, _id: -1},且sort()顺序严格一致,否则无法走索引 - 第一页查全量:
db.collection.find({}).sort({createdAt: -1, _id: -1}).limit(20) - 后续页传两个游标值:
last.createdAt和last._id,查询条件写成:db.collection.find({$or: [{createdAt: {$lt: last.createdAt}}, {createdAt: last.createdAt, _id: {$lt: last._id}}]}).sort({createdAt: -1, _id: -1}).limit(20) - 前端必须保存并透传这两个值,只记一个会导致重复或跳过
- 如果
createdAt是用户可控字段(如前端传入),需校验其合理性,避免被恶意构造导致游标失效
_id 游标方案为何最简可靠
默认 ObjectId 类型的 _id 天然满足:唯一、单调递增、不可变、写入时间隐含其中。只要按 _id 升序或降序分页,就能避开排序开销和重复风险。
- 第一页:
db.collection.find({}).sort({_id: 1}).limit(20) - 后续页:
db.collection.find({_id: {$gt: last_id}}).sort({_id: 1}).limit(20) -
_id字段默认有索引,无需额外操作 - 严禁手动修改或重写
_id(例如删+插),会破坏时间连续性,导致游标错位 - 如果某次返回不足 20 条,说明已到底;不要 fallback 到
skip()补足,那等于回到低效原点
真正容易被忽略的是:游标分页不是“换种写法”,而是业务交互模式的切换。它不支持跳转任意页码(比如直接输“第 87 页”),前端必须按顺序翻页、持久化游标值。一旦设计成带页码的 REST API,底层再怎么优化也绕不开 skip 的本质瓶颈。










