skip(n)本质是顺序遍历丢弃前n条,无法跳过中间数据,配合无索引sort会触发inmemorysort导致性能崩盘;应改用游标分页并建立覆盖索引。

因为 skip(N) 不是“跳转”,而是“扫描并丢弃前 N 条”——N 越大,CPU 和内存开销越线性增长,尤其配合无索引 sort 时会触发 inMemorySort,性能直接崩盘。
skip() 的执行本质是顺序遍历,不是随机访问
MongoDB 没有“直接定位第 N 条记录”的能力。执行 skip(100000) 时,它必须从查询结果集开头逐条加载、判断、再丢弃前 10 万条文档。这个过程无法跳过中间数据,也不能靠索引加速“跳过”动作本身。
- 即使
createdAt字段建了索引,sort({ createdAt: -1 }).skip(100000)仍要先用索引取出全部匹配文档(或足够多),再顺序跳过前 10 万条 - explain("executionStats") 中
totalDocsExamined会远大于nReturned,比如返回 20 条却扫描了 10 万+ 条,就是典型信号 - WiredTiger 引擎下,大量 skip 还会加剧 page fault 和 cache eviction,进一步拖慢后续查询
sort() + skip() 组合是双重暴击
如果 sort() 字段没走索引,MongoDB 就得先把所有匹配文档拉进内存排序(stage: inMemorySort),然后再做 skip —— 此时 skip 成本不是 O(N),而是 O(N + M),M 是排序所需内存和时间。
- 常见错误:只对
status建索引,但查询是{ status: "active" }+.sort({ createdAt: -1 })→createdAt无索引,必触发 inMemorySort - 正确做法:建复合索引
db.collection.createIndex({ status: 1, createdAt: -1, _id: 1 }),让 sort 和查询都覆盖 - 务必包含
_id:避免createdAt相同时排序不稳定,导致分页错位或重复
写入活跃场景下,skip 分页结果不可靠
用户翻页过程中,新文档插入或旧文档删除,会导致同一 skip(N) 值返回不同内容 —— 不是“慢”,而是“错”。这不是 bug,是 skip 语义决定的。
- 第 1 页查出 [A, B, C, D],此时插入 E(排在 A 前);第 2 页
skip(4)可能跳过 E,也可能把 B 算成第 5 条,结果漏掉或重复 - explain 显示
nReturned波动大,但totalDocsExamined稳定高,说明数据在动,而 skip 逻辑没感知 - 游标分页靠的是“上一页末尾值”,天然适应动态数据集;skip 分页靠的是“固定偏移量”,只适合静态快照
真正卡住的从来不是代码怎么写,而是你还在用页码思维去驱动一个不支持页码语义的数据库。游标分页不是“更高级的技巧”,它是对 MongoDB 查询模型的基本尊重 —— 记住位置,而不是数数。











