$group会直接爆100mb内存,因其是阻塞阶段,需将所有匹配文档全加载进内存才能分组;若无前置$match过滤且未精简字段,易超限,须配合allowdiskuse: true并确保分片键与$_id匹配。

为什么$group会直接爆100MB内存
$group是阻塞阶段,必须把所有匹配文档全加载进内存才能开始分组。如果前面没做任何过滤,它就会把整个集合的原始文档(含所有字段)一股脑拉进来——哪怕只是统计个数量,一个20KB的文档扫5000条就超100MB。错误信息"Exceeded memory limit for $group, but didn't allow external sort"不是说数据太大,而是MongoDB发现你没给它“写磁盘”的许可,又没法硬扛下去。
必须前置$match并精简字段
别让$group当第一个阶段。高选择性条件(如时间范围、状态码、业务ID前缀)一定要用$match放在最开头;紧接着用$project只保留$group真正需要的字段,比如只留user_id和amount,别把description或logs数组一起拖进来。
- 错:先
$unwind再$group,且没$match前置 → 全量展开后分组,内存翻N倍 - 对:先
$match筛出近30天数据,再$project只取category和price,最后$group - 复合筛选别堆在$group后面:想查“订单数>5的用户”,不要
{$group: {_id: "$user_id", cnt: {$sum: 1}}}, {$match: {cnt: {$gt: 5}}},而要提前用$match限定基础数据集
allowDiskUse: true不是万能药,但必须加
在聚合调用末尾显式传入{allowDiskUse: true},这是绕过100MB内存墙最直接的手段。但它只在你真正需要时起作用——比如分片集群上,mongos不会自动把该选项转发到各shard,所以应用端发起聚合时,这个参数必须作为第二个参数传进去,不能漏。
- Shell里:
db.orders.aggregate([{$match: {status: "paid"}}, {$group: {_id: "$region", total: {$sum: "$amt"}}}], {allowDiskUse: true}) - Node.js驱动:确保options对象里有
allowDiskUse: true,不是塞进pipeline数组 - Java驱动:
AggregationOptions.builder().allowDiskUse(true).build() - 注意:它解决的是内存限制,不解决16MB BSON文档大小上限;结果太大仍需用
$out写入新集合
唯一值统计等场景慎用$addToSet
用$addToSet配合$size统计唯一值数量(如{$group: {_id: null, unique_count: {$size: {$addToSet: "$status"}}}})看似简洁,但MongoDB真会尝试把所有去重后的值暂存在内存里。基数稍高(比如status有上万种取值)就容易触发内存错误。
- 更稳的做法:先
{$group: {_id: "$status"}}建空分组,再套一层{$group: {_id: null, unique_count: {$sum: 1}}}计行数 - 如果字段基数低但数据量大,加
allowDiskUse: true通常就能过 - 分片集群下跑这类聚合,同样要传
allowDiskUse: true,否则各shard本地执行时照样报错
$group的_id字段不匹配。比如按{tenant_id: 1, user_id: 1}分片,却用{$group: {_id: "$created_at"}},mongos只能把所有shard的数据拉到自己内存里归并——这时allowDiskUse: true对mongos无效,它根本不支持磁盘归并。











