$group 和 $sort 会卡住管道,因二者均为阻塞式阶段,需等待上游全部文档到达;若上游无过滤或投影,全量数据将涌入内存,易触发内存超限或oom。

为什么 $group 和 $sort 会卡住整个管道
$group 和 $sort 都是阻塞式阶段,它们必须等上游所有文档全部到达后才开始处理。一旦上游没过滤、没投影,就等于把全量数据拖进内存排队——尤其在 MongoDB 7.0 中,WiredTiger 默认内存限制更严格,容易触发 errmsg: "Sort exceeded memory limit of 104857600 bytes" 或直接 OOM kill。
常见错误现象包括:聚合耗时突增但 nscannedObjects 与 nreturned 比值正常、explain("executionStats") 显示 stage: "SORT" 或 "GROUP" 的 executionTimeMillis 占比超 80%、mongostat 显示 netIn 突升但 conn 无明显增长。
- 不要把
$sort放在$match前;更不要放在$limit后(那只是对前 N 条排序) -
$group前必须有强过滤条件,否则_id表达式再简单,也会因输入膨胀导致内存翻倍 - MongoDB 7.0 对
allowDiskUse: true的行为更保守:即使开启,若中间结果含大文档(如嵌套数组未裁剪),仍可能拒绝落盘而报错
用 system.profile 定位哪个阶段真正拖慢了 pipeline
Profiling 记录的是真实端到端延迟,比 explain() 更反映线上瓶颈。在 MongoDB 7.0 中,system.profile 仍默认存在,但需手动启用且注意权限:
- 执行
db.setProfilingLevel(1, {slowms: 200})(生产环境建议 ≥200ms,避免日志刷爆) - 查慢聚合:
db.system.profile.find({"command.aggregate": {$exists: true}, "millis": {$gt: 200}}).sort({"ts": -1}).limit(3) - 重点看返回文档里的
command.aggregate数组顺序,结合millis和nscannedObjects判断:如果第 2 个阶段耗时占总耗时 90%,但nscannedObjects很小,大概率是上一阶段输出了大量冗余字段(比如没$project就进了$group)
⚠️ 注意:millis 包含网络往返和锁等待,所以它比 explain("executionStats").executionStages.executionTimeMillis 更高是正常的——这恰恰说明瓶颈不在纯计算,而在数据流转或并发争抢。
$project 和 $unset 不是性能银弹,但放错位置会白忙
MongoDB 7.0 的聚合优化器确实会自动下推 $match 过滤器,但仅限于「不依赖投影计算结果」的字段。比如你写了:
db.orders.aggregate([
{ $addFields: { total: { $sum: "$items.price" } } },
{ $project: { orderId: 1, total: 1 } },
{ $match: { total: { $gt: 1000 } } }
])
优化器能把 {orderId: "xxx"} 这类原始字段的 $match 提前,但 total 是计算字段,它的 $match 只能留在原地——也就是说,$addFields 仍要处理全量文档。
- 若业务允许,把过滤逻辑尽量下沉到
$match阶段,用原始字段(如status,createdAt)代替计算字段 -
$project放在$group或$sort前,能显著减少内存占用;但它放在管道开头对性能几乎没提升(优化器已自动做字段剪枝) - 避免用
$unset: ["bigField", "hugeArray"]替代$project:前者仍需解析并丢弃,后者直接跳过读取,开销更低
排查时别忽略 WiredTiger 缓存与分片路由开销
MongoDB 7.0 默认使用 WiredTiger 存储引擎,其 cache size 由 wiredTigerCacheSizeGB 控制。当聚合涉及大量随机读(如未命中索引的 $lookup),缓存击穿会导致磁盘 I/O 暴涨,此时 system.profile 的 millis 会虚高,但 keysExamined 却很低。
- 检查
db.serverStatus().wiredTiger.cache中的"bytes currently in the cache"和"maximum bytes configured"比值,长期 >90% 就该调大缓存 - 分片集群中,若
$lookup目标集合未按shard key分片,或管道里混用$facet+ 跨分片查询,mongos 会拉回大量中间结果再聚合——这时system.profile里command.aggregate的shards字段会显示多个分片参与,且各分片millis差异极大 - 用
db.runCommand({aggregate: 1, pipeline: [...], explain: "executionStats"})查看shards下每个分片的executionStages,确认是否某一分片承担了全部$sort或$group
真正卡住聚合的,往往不是某个 stage 写得不对,而是 stage 之间的数据体积失控、缓存没兜住、或者分片没切好——这些细节在 system.profile 和 serverStatus 里才有迹可循。











