连续$unwind三层以上易触发oom,因每层指数级生成中间文档;$function是mongodb 5.0+唯一支持未知深度嵌套递归的方案,需注意递归终止、路径传参及后续$unwind。

直接用 $unwind 连续展开三层以上嵌套,基本等于主动触发 OOM;$function 是 MongoDB 5.0+ 唯一能真正处理未知深度嵌套的方案。
为什么连续 $unwind 会崩掉内存
假设一个文档结构是 root → kids → kids → kids → ...,每层平均 5 个子节点,4 层就生成 5⁴ = 625 份中间文档;5 层就是 3125 份。这些文档全在内存里流转,聚合还没走到 $group 就被 errmsg: "Exceeded memory limit..." 终止。
常见错误现象:
-
$unwind没加preserveNullAndEmptyArrays: true,导致含空kids的文档直接消失 - 写死 4 层
$unwind,但某条数据有 5 层,深层节点永远查不到 - 用
$reduce模拟递归,但无法动态访问item.kids—— 它只认静态数组,不支持路径跳转
$function 是怎么递归展平的
它本质是聚合管道内嵌的一个 JS 函数,可调用自身,且参数传入的是字段路径(不是字符串),所以能真正“钻”进任意深度。
实操要点:
- 函数
body必须返回扁平数组,比如[{_id:"a",sales:100}, {_id:"b",sales:200}],不能返回嵌套结构 -
args里传路径必须用"$kids",写成"kids"就拿不到值 - 终止条件必须防 null/undefined:
if (!Array.isArray(kids) || kids.length === 0) return out; - 生产环境要确认
javascriptEnabled: true,Atlas 集群可能默认禁用(查db.runCommand({ getCmdLineOpts: 1 }))
示例片段:
db.nodes.aggregate([
{ $match: { _id: "ABC" } },
{
$set: {
flattenedKids: {
$function: {
body: function(kids, out) {
if (!Array.isArray(kids) || kids.length === 0) return out;
for (let kid of kids) {
out.push({ _id: kid._id, sales: kid.sales });
if (Array.isArray(kid.kids)) {
arguments.callee(kid.kids, out); // 递归调用
}
}
return out;
},
args: ["$kids", []],
lang: "js"
}
}
}
},
{ $unwind: "$flattenedKids" },
{ $group: { _id: "$_id", totalSales: { $sum: "$flattenedKids.sales" } } }
])
$graphLookup 不适用于单文档内嵌套
$graphLookup 只能跨文档递归(比如父子 ID 分散在不同文档),对单文档内的 kids: [{ kids: [...] }] 完全无效。它根本看不到内部字段,只会把整个 kids 数组当黑盒。
如果你误用了,会发现 descendants 字段始终为空,或只返回顶层节点 —— 因为 connectFromField: "parent_id" 在当前文档里根本不存在这个字段。
这种场景下硬套 $graphLookup,不如直接放弃聚合,在应用层做 DFS。
真正麻烦的不是写不出递归逻辑,而是忘了 $function 返回的数组需要再 $unwind 一次才能进 $group;很多人卡在这步,以为函数没生效,其实只是漏了后续展开。











