$match必须前置、$project需精简字段、$sort/$group前须过滤,否则因全量加载文档导致内存溢出;索引仅对管道开头$match生效,allowdiskuse仅为兜底而非优化解法。

绝大多数 Exceeded memory limit for $group 或 Sort exceeded memory limit 错误,不是数据量太大,而是聚合管道写法触发了内存暴增——$match 没前置、$project 没精简、$sort/$group 过早执行。
为什么 $sort 和 $group 会直接爆内存
这两个阶段必须把所有输入文档全加载进内存才能开始工作。如果前面没过滤,MongoDB 就得把原始文档(含长文本、数组、二进制字段)一股脑拉进来。比如单文档平均 15KB,7000 条就超 100MB。
-
$group前没$match:按user_id统计,却没限定created_at时间范围,扫三年订单 -
$sort前没$project:只用updated_at排序,但文档带logs字段(几 MB 文本),全被拖进内存 - 复合条件写在
$group后:比如{$group: {}}}再{$match: {total: {$gt: 10000}}},分组已完成,内存早满了
必须把 $match 放最前面,且用高选择性条件
MongoDB 只有管道开头的 $match 能走索引;后面的 $match 是内存过滤,不减少扫描量。
- 优先用区分度高的字段:如
status: "completed"、shop_id: "abc123",避免只用type: "order" - 复合查询必须建对应复合索引:若管道开头是
{$match: {shop_id: 1, created_at: {$gte: ...}}},索引就得是db.orders.createIndex({shop_id: 1, created_at: 1}) - 别指望优化器自动重排:
$lookup和$unwind会阻断$match下推,它们前面的$match必须手动写好
$project 是内存控制开关,不是可选项
加一个 $project 放在 $sort 或 $group 前,常能砍掉 70%+ 中间数据体积。
- 只保留后续阶段真正需要的字段:比如按
category分组求和,$project里只需{category: 1, amount: 1, _id: 0} - 显式排除
_id:除非分组键依赖它,否则加_id: 0,百万条省下 12MB - 别在
$project里算新字段再用于$match:像{avgScore: {$avg: "$scores"}}+{$match: {avgScore: {$gt: 80}}},这会让前置过滤失效
allowDiskUse 是兜底,不是解法
allowDiskUse: true 只在磁盘 I/O 允许、临时空间充足、且集群支持时才有效——MongoDB Atlas M0 免费版根本禁用它。
- 它不提升性能,只是把内存压力转成磁盘压力;线上实时接口用它,延迟可能飙升数秒
- 必须写在
aggregate()调用末尾,不能塞进某个 stage 里 - 即使开了,单文档仍受 16MB BSON 限制;结果太大,得改用
$out写入集合
真正难处理的,是那些字段结构松散、嵌套深、又无法加索引的场景——这时候 $project 的字段裁剪和 $match 的位置,就是唯一可控的杠杆。











