直接用 skip() + limit() 在大数据量下变慢是因为 skip() 需从头扫描并丢弃前 n 条文档,无法跳过遍历,导致 cpu 和 i/o 压力线性上升;应改用基于排序字段的游标分页,并确保索引、查询条件与排序方向一致。

为什么直接用 skip() + limit() 在大数据量下会变慢
因为 skip() 不是“跳到第 N 条”,而是“从头开始扫描,逐个计数,丢弃前 N 条”。当 skip(100000) 时,MongoDB 仍需读取并跳过前 10 万条匹配文档——哪怕你只想要后面 10 条。索引能加速定位,但无法绕过这个遍历丢弃过程。CPU 和 I/O 压力随 skip 值线性上升,响应时间可能从毫秒级涨到秒级甚至超时。
必须为分页字段建索引,且排序字段和查询条件要对齐
范围分页(游标分页)依赖排序字段的单调性和可比较性,比如 _id、created_at、seq_id。但光有字段不够,必须确保:
• 该字段在查询中被显式 sort(),且方向一致(如始终 {created_at: 1});
• 所有分页查询都带相同前置条件(例如固定 {status: "active"}),否则游标值跨条件失效;
• 该字段已建升序/降序索引,且索引前缀覆盖查询条件(如 db.collection.createIndex({status: 1, created_at: 1}))。
漏掉任意一点,查询可能走全表或返回重复/遗漏数据。
find({created_at: {$gt: "2025-06-01T00:00:00Z"}}).sort({created_at: 1}).limit(10) 这类查询才是真·游标分页
这不是“替代 skip 的写法”,而是换了一种分页范式:不靠页码,靠上一页最后一条的排序键值。
• 第一页:先查 find().sort({created_at: 1}).limit(10),拿到第 10 条的 created_at 值(记为 last_time);
• 下一页:查 find({created_at: {$gt: last_time}}).sort({created_at: 1}).limit(10);
• 注意:必须用 $gt(不能用 $gte),否则可能重复返回边界值那条;
• 如果排序字段可能重复(如多个文档同秒创建),需叠加唯一字段兜底,例如 {created_at: 1, _id: 1},查询条件也补上 $gt 复合值。
文档模型里预留“分页友好字段”比事后补索引更有效
设计阶段就该考虑分页路径,而不是等慢了再优化。
• 避免用业务字段(如 title、content)做分页主键——它们可能重复、无序、未索引;
• 主动添加轻量唯一递增字段(如 seq_id: NumberLong),比依赖 ObjectId 时间戳更可控;
• 对含状态流转的数据(如订单),把关键过滤字段(status、updated_at)和排序字段一起组成复合索引;
• 写入时保证 created_at 是服务器时间(非客户端传入),避免因时钟偏差导致分页错乱。











