先用$unwind展开外层数组,再用$unwind展开内层数组,最后$group+$addtoset去重;或用$reduce+$setunion合并去重,需配合$ifnull处理空值。

用 $unwind 展开嵌套数组再去重
直接对嵌套数组字段用 $group + $addToSet 会失败,因为 $addToSet 不接受数组作为输入值(报错 cannot use $addToSet on field 'xxx' with value: [[...], [...]])。必须先展开一层。
假设文档结构是:{ name: "A", tags: [ ["web", "js"], ["web", "node"] ] },目标是提取所有不重复的 tag 字符串(即 ["web", "js", "node"]):
- 先用
$unwind处理外层数组:{ $unwind: "$tags" },得到每条记录带一个子数组(如["web", "js"]) - 再用一次
$unwind处理内层数组:{ $unwind: "$tags" },此时$tags变成单个字符串 - 最后
$group收集:{ $group: { _id: null, uniqueTags: { $addToSet: "$tags" } } }
$reduce + $setUnion 适合浅层嵌套且不想打散文档流
如果嵌套只有一层(比如 categories: [["a","b"], ["b","c"]]),且你希望避免 $unwind 导致文档数量爆炸(尤其大数据集),可用 $reduce 合并数组再去重。
关键点是:$setUnion 要求输入是多个数组,而 $reduce 可以把 categories 这个二维数组逐个合并:
{
$reduce: {
input: "$categories",
initialValue: [],
in: { $setUnion: ["$$value", "$$this"] }
}
}
这个表达式返回一个一维去重数组(["a","b","c"])。注意:$setUnion 自动去重,但只对顶层元素有效;它不会递归扁平化,所以输入必须已是“数组的数组”形式。
处理空数组或缺失字段时加 $ifNull 防崩
一旦某个文档的嵌套数组字段为 null、undefined 或空数组 [],$unwind 会跳过该文档(默认行为),$reduce 则会报错 input to $reduce must be an array。
- 用
$unwind前补一层:{ $addFields: { tags: { $ifNull: ["$tags", []] } } } - 用
$reduce前确保输入安全:input: { $ifNull: ["$categories", []] } - 如果字段可能根本不存在,
$ifNull第二个参数设为空数组比设[]更稳妥(MongoDB 会统一处理)
性能差异:$unwind 适合小数据,$reduce 更省内存但语法难写对
$unwind 在内存和时间上都是 O(n×m) 复杂度(n 是文档数,m 是平均子数组长度),容易触发 Exceeded memory limit 错误;$reduce 是单文档内计算,不放大文档流,但要求你准确写出累加逻辑。
真实场景中容易忽略的是:嵌套层级判断。比如 items: [{ props: ["x","y"] }, { props: ["y","z"] }] —— 这不是二维数组,而是对象数组里的字段数组,得先 $map 提取再 $reduce,不能直接套用上面任一方案。











