$group和$sort会爆内存是因为必须将全部输入文档加载进内存才能工作,若前置无$match过滤和$project精简,原始大文档(如logs、binary)全量加载导致超限;索引需严格匹配$match字段顺序与类型,$project是关键内存控制手段。

为什么$group和$sort会直接爆内存
$group和$sort必须把全部输入文档加载进内存才能开始工作。如果前面没过滤,MongoDB就会把原始文档(含logs长文本、binary字段、嵌套数组)全拉进来。单文档平均15KB,7000条就超100MB——错误不是数据总量大,而是管道没“瘦身”。
- 常见错误现象:
Exceeded memory limit for $group或Sort exceeded memory limit - 典型诱因:$group前没$match限定时间范围;$sort前没$project裁剪掉大字段
- 注意:$lookup和$unwind会阻断$match下推,它们前面的$match必须手动写好,不能指望优化器自动补
$match必须放最前面,且字段顺序要对得上索引
MongoDB只对聚合管道中「第一个能走索引的$match」生效;后面的$match全是内存过滤,不减少扫描量。索引字段顺序必须严格匹配$match中查询键的顺序,否则可能跳过索引。
- 正确写法:
{$match: {shop_id: "abc123", created_at: {$gte: ISODate("2024-01-01")}}}→ 对应索引db.orders.createIndex({shop_id: 1, created_at: 1}) - 错误写法:
{$match: {created_at: {$gte: ...}, shop_id: "abc123"}}→ 索引大概率失效 - 类型要一致:索引是
ObjectId,查询传字符串ID,索引直接被忽略
$project是内存控制开关,不是可选项
加一个$project放在$sort或$group前,常能砍掉70%+中间数据体积。它不是为了“返回什么”,而是为了“让后续阶段少处理什么”。
- 只保留后续真正需要的字段:比如按
category分组求和,$project里只需{category: 1, amount: 1, _id: 0} - 显式排除
_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写入集合 - 生产环境建议始终显式传
{allowDiskUse: true},别依赖静默行为——但前提是已做完$match前置、$project精简、索引对齐这些真正有效的优化











