$graphlookup适合扁平结构查子孙并汇总,$function适合嵌套结构递归展平;前者需防字段写反、缺restrictsearchwithmatch和索引,后者须避免副作用、内存爆炸及非法数据库访问。

用 $graphLookup 查子孙并汇总,或用 $function 递归遍历后 $group——前者适合“查树+统计”,后者适合“展平+聚合”,选错阶段会导致内存爆掉或漏数据。
用 $graphLookup 做递归汇总(查某节点下所有子孙的总金额)
这是最常用、也最安全的递归汇总方式,前提是你的数据是扁平结构(每个节点独立文档,带 parent_id 字段)。
常见错误现象:
-
connectFromField和connectToField写反,结果为空数组 - 没加
restrictSearchWithMatch,把已删除节点也拉进来,汇总值虚高 - 没建索引,
parent_id字段全表扫描,10万条数据查 20 秒
实操要点:
- 方向必须是:从当前节点出发,找
parent_id等于它的子节点 → 所以设connectFromField: "parent_id",connectToField: "_id" - 务必加
maxDepth: 5(按你业务最大层级设,别写 100) - 在
restrictSearchWithMatch里过滤有效状态:{ status: "active" } - 执行前先建索引:
db.nodes.createIndex({ parent_id: 1 })
示例(查 ID 为 ABC 的分类下所有活跃子节点的销售总额):
db.nodes.aggregate([
{ $match: { _id: "ABC" } },
{
$graphLookup: {
from: "nodes",
startWith: "$_id",
connectFromField: "parent_id",
connectToField: "_id",
as: "descendants",
maxDepth: 5,
restrictSearchWithMatch: { status: "active" }
}
},
{ $unwind: "$descendants" },
{ $group: { _id: "$_id", totalSales: { $sum: "$descendants.sales" } } }
])
用 $function 递归汇总嵌套 kids 字段(文档内嵌套,非跨文档)
当你的树形结构是单文档内嵌套(如 kids: [{ _id: "a", sales: 100, kids: [...] }]),且深度未知,$graphLookup 完全不适用——只能用 $function 先展平再汇总。
为什么不用连续 $unwind?它会爆炸式生成中间文档。3 层嵌套、每层 10 个子节点,就产生 1000 份文档,内存直接打满。
实操要点:
-
body函数必须返回一个扁平数组(比如只取_id和sales),不能有副作用(如console.log) -
args里传路径要用"$kids",不是字符串"kids" - 递归终止条件要防
null/undefined:if (!Array.isArray(kids) || kids.length === 0) return out; - 汇总必须在
$function之后做,不能在函数体内算总数(聚合阶段不共享变量)
示例(对根文档的 kids 递归提取所有 sales 并求和):
db.collection.aggregate([
{
$set: {
allSales: {
$function: {
body: "function flattenAndSum(kids, acc) { " +
" if (!Array.isArray(kids) || kids.length === 0) return acc; " +
" for (let elem of kids) { " +
" acc += (elem.sales || 0); " +
" flattenAndSum(elem.kids ?? [], acc); " +
" } " +
" return acc; " +
"}",
args: ["$kids", 0],
lang: "js"
}
}
}
}
])
汇总结果要分组或关联时,别在 $function 里硬编码逻辑
$function 只负责“递归遍历+收集”,后续的 $group、$lookup、$project 必须拆到管道后续阶段。在函数里拼接对象或调用 db.collection.findOne() 是非法的,会报 ReferenceError: db is not defined。
容易踩的坑:
- 想在
body里直接查另一个集合 → 不支持,$function是纯 JS 沙箱,无数据库访问能力 - 把整个子文档 push 进数组,导致后续
$group时字段路径混乱(比如$sum: "$allKids.sales"失败,因为allKids是对象数组,不是数值数组) - 返回结构不统一:有的节点没
sales字段,elem.sales是undefined,加法变成NaN
正确做法:在 $function 中只返回干净的数值或标准对象(如 { _id: "...", sales: 100 }),再用 $unwind + $group 或 $sum 统计。
真正麻烦的不是写法,而是嵌套深度是否稳定、parent_id 索引有没有建、$function 的 JS 引擎有没有被 Atlas 集群禁用——这些地方一漏,查询就静默失败或超时。











