skip() + limit() 在大数据量下变慢是因为 skip(N) 需扫描并丢弃前 N 条文档,N 越大开销越高;配合无索引 sort 会触发内存排序,使性能呈 O(N) 下滑。
因为 skip(N) 不是“直接跳到第 N 条”,而是让 MongoDB 先扫描、加载并丢弃前 N 条匹配文档。N 越大,CPU 和内存开销越高,尤其是配合 sort() 时——如果排序字段没走索引,还要额外做内存排序,再叠加 skip,性能呈 O(N) 下滑。
常见错误现象包括:
explain("executionStats") 显示 nReturned 很小,但 totalDocsExamined 高达几十万MongoDB 执行 sort() + skip() + limit() 的固定顺序是:先排序 → 再 skip → 最后 limit。如果排序无法利用索引,就会触发 inMemorySort,而内存排序无法跳过中间数据,导致 skip 成本爆炸。
正确做法是让排序字段组合索引覆盖查询条件和排序需求:
createdAt),建单字段索引:db.collection.createIndex({ createdAt: -1 })
{ status: 1, createdAt: -1 }),索引字段顺序必须严格匹配,且包含查询中用到的等值字段(如 status: "active")_id 字段去重,例如 .sort({ status: 1, createdAt: -1, _id: 1 }),否则重复值会导致分页错位游标分页本质是“记住位置”,不是“跳过多少条”。它把上一页最后一条文档的关键排序字段值(如 createdAt 和 _id)作为下一页的查询起点,绕过 skip 的扫描开销。
示例(按 createdAt 降序分页):
db.collection.find({ $or: [ { createdAt: { $lt: ISODate("2024-01-01T10:00:00Z") } }, { createdAt: ISODate("2024-01-01T10:00:00Z"), _id: { $lt: ObjectId("...") } } ]}).sort({ createdAt: -1, _id: -1 }).limit(10)
要点:
createdAt,必须联合 _id 处理时间相同的情况createdAt 和 _id,而不是页码游标分页不是“开了就快”,它依赖几个隐性前提:
createdAt 比用用户自填的 updatedAt 更可靠)explain() 确认 executionStats.executionStages.stage === "IXSCAN"
mongodb-cursor-pagination),注意它默认编码游标值,但解码失败时不会报错,而是静默返回空结果——务必校验游标参数合法性