$skip和$limit必须置于聚合管道末尾,因它们仅作用于前序阶段输出流;前置会导致数据截断、分页错乱;$sort须在$skip前以保证排序稳定;深度分页应改用游标分页。

聚合管道里用$skip和$limit必须写在最后阶段
因为聚合管道是顺序执行的,$skip 和 $limit 只能作用于前一阶段输出的文档流。如果把它们放在 $group 或 $lookup 前面,会跳过/截断原始数据,而不是聚合后的结果。
常见错误现象:$skip 没生效、分页总数对不上、第二页开始数据重复或丢失。
- 正确顺序:所有过滤、分组、展开阶段 →
$sort(如有)→$skip→$limit -
$sort必须在$skip之前;否则排序发生在截断之后,分页逻辑就乱了 - 如果没加
$sort,$skip的行为依赖默认文档顺序(通常是插入顺序),但不保证稳定
分页参数要动态算,别硬编码 offset
页码从 1 开始时,$skip 的值不是页码本身,而是 (pageNum - 1) * pageSize。这个计算必须在应用层完成,不能交给 MongoDB。
使用场景:API 接收 page=3&size=20,后端应生成:{$skip: 40, $limit: 20}。
- 错误写法:
{$skip: "$page", $limit: "$size"}—— 聚合里不支持变量插值 - 避免在聚合里用
$expr动态算$skip,MongoDB 不允许 - 注意整数溢出:当
pageNum很大(比如百万级页码),(pageNum - 1) * pageSize可能超 32 位整数范围,Node.js 或 Java 客户端需确保传入的是安全整数
性能差的根本原因:$skip 扫描全量中间结果
$skip 在聚合中依然要逐条跳过前面的文档,不管这些文档是否已被 $group 或 $project 处理过。它不会“跳过已聚合的桶”,而是跳过最终流水线上的每一条输出文档。
这意味着:即使 $group 后只剩 100 条,$skip: 10000 仍会报错或极慢——因为 MongoDB 还是得把前 10000 条“逻辑上存在”的输出档都生成出来再丢弃。
- 典型卡点:聚合后总结果才几百条,但
$skip设成 5000,查询直接 hang 住 - 没有索引能优化
$skip本身;它只依赖前序阶段的输出量 - 真实瓶颈常在
$lookup或$unwind阶段放大了中间数据量,让$skip更吃力
替代方案:游标分页比 $skip/$limit 更可靠
当页码靠后(如 >1000)或总数据量大时,应放弃 $skip/$limit,改用基于字段值的范围分页,比如用上一页最后一条的 _id 或时间戳继续查。
例如,上一页最后一条是 {"_id": ObjectId("..."), "createdAt": ISODate("2026-05-10T08:00:00Z")},下一页就用:
db.orders.aggregate([
{ $match: { createdAt: { $lt: ISODate("2026-05-10T08:00:00Z") } } },
{ $sort: { createdAt: -1 } },
{ $limit: 20 }
])
- 必须确保排序字段有索引,否则
$match + $sort会全表扫 - 不能用
$skip模拟“第 N 页”,而要客户端维护游标位置(比如把lastId当作分页 token) - 这种模式天然规避了深度偏移问题,但要求 API 支持“上一页/下一页”而非“跳转任意页”
真正难处理的不是语法,而是聚合后结果集大小不可控——$skip 看似简单,但它暴露的是整个流水线的设计水位。先想清楚要不要分页、谁来承担排序稳定性、下游能否接受游标模型,比急着写 $skip 重要得多。











