$skip在聚合管道中需全量扫描,无法跳过未进入当前阶段的文档,分片环境下各shard独立执行导致性能急剧下降;正确替代方案是游标分页+复合索引。

$skip在聚合管道中仍需全量扫描
聚合管道里的$skip和$limit不是“跳过并取”,而是每个阶段都必须把上游所有文档拉进来再丢弃前N条。哪怕你已经在$match里筛掉99%的数据,$skip仍要逐条计数——它无法跳过未进入当前阶段的文档,更不能利用索引直接定位。
分片集群下$skip会放大性能损耗
在shard环境中,$skip(100000)不是由mongos统一跳过,而是每个shard各自执行$skip(100000)再合并结果。如果某shard实际只匹配到5万条,它还得硬扫更多数据凑够10万跳过量;网络传输、内存排序、二次合并全被拖慢。常见现象是单机查200ms,3分片变2s+,且页码越深越卡。
聚合阶段顺序错误让$skip雪上加霜
$skip位置不对会极大加剧开销:
- 放在
$match之后但$project之前:仍要加载全部字段(比如含大文本或二进制字段)进内存计数 - 没配
allowDiskUse: true:大数据量$skip容易触发Exceeded memory limit - 没加
sort就$skip:结果无序,翻页时数据重复或遗漏,用户感知为“页面错乱”
替代方案必须用范围查询+复合索引
真正可行的替换不是“换个写法”,而是改查询模型:
- 首请求走
find().sort({ createdAt: -1, _id: -1 }).limit(20),拿到最后一条的{ createdAt, _id } - 下一页用
find({ $or: [ { createdAt: { $lt: last.createdAt } }, { createdAt: last.createdAt, _id: { $lt: last._id } } ] }) - 对应索引必须是
{ createdAt: -1, _id: -1 },且createdAt字段要有高区分度——时间戳精度不够时,_id是兜底关键
游标分页不支持跳转任意页,但它把每次查询压到索引B-Tree的单次范围扫描,这才是百万级数据下唯一稳定的路径。











