$group中\_id必须显式指定,否则报错;$push多列需用对象包装避免嵌套数组;空数组处理需preserveNullAndEmptyArrays;$addToSet去重不保序且不比较结构等价性。

用 $group + $push 合并多列时,_id 必须显式指定
不写 _id 会直接报错:"unknown group key: null"。哪怕你想把所有文档合并成一行,也得给 _id 设一个固定值,比如 null 或字符串 "all"。
常见错误是直接省略或写成空对象:{_id: {}} 不合法;{_id: null} 是允许的,但后续字段引用要小心——不能用 $null.field 这种写法。
-
_id: "$customer_id"→ 按客户 ID 分组,每组一行 -
_id: null→ 所有文档归为一组,最终只输出一行 -
_id: "total"→ 语义清晰,且避免和字段名冲突
$push 收集多列时,必须用对象字面量包装字段
如果直接写 {interests: {$push: "$interests"}},只能拿到原始数组的嵌套副本(比如 [["reading"], ["gaming"]]),不是你想要的扁平化结果。
正确做法是用对象结构包裹每列,让每个文档的字段成为新数组中的一项:
db.users.aggregate([
{
$group: {
_id: "$name",
merged: {
$push: {
hobby: "$hobby",
level: "$level",
since: "$since"
}
}
}
}
])
输出类似:{"_id":"Alice","merged":[{"hobby":"reading","level":"expert","since":"2022"},{"hobby":"gaming","level":"beginner","since":"2023"}]}
- 不加包装 → 得到的是数组的数组,无法直接映射字段
- 用
$push: {key: "$field"}→ 每个元素是独立对象,结构可控 - 字段名任意,但别和内置操作符(如
$$ROOT)重名
合并后想展开再重组?小心 $unwind 和 $group 的顺序陷阱
有人想先 $unwind 再 $group 来“打散再聚合”,这在处理嵌套数组时容易翻车:如果原始文档里 items 是空数组或 null,$unwind 默认会丢弃整条文档(除非加 preserveNullAndEmptyArrays: true)。
更稳妥的做法是跳过 $unwind,直接在 $group 阶段用 $push 收集完整文档或字段对象。
- 原始数据含空数组?加
{preserveNullAndEmptyArrays: true}才能保底 - 只是合并字段值,不用展开 → 完全不需要
$unwind - 用了
$unwind就必须跟$group配对,否则数据行数会爆炸
$addToSet 能去重,但不保证顺序,也不处理深层结构
如果你用 $push 后发现重复项太多,自然想到换 $addToSet。但它只对**整个被推入的值**做唯一性判断。
比如 $addToSet: "$tags" 对 ["a","b"] 和 ["b","a"] 视为不同;$addToSet: {tag: "$tag", score: "$score"} 中只要任一字段不同,就视为新元素。
- 数组内容相同但顺序不同 → 不去重
- 对象字段顺序不同(如
{a:1,b:2}vs{b:2,a:1})→ MongoDB 认为不等 - 真要去重又保序?得在应用层用
distinct或额外加$sort+$reduce
_id 的强制要求、对象包装的必要性,以及空值/重复值的隐式行为。这些点不手动验证,光看示例代码很容易跑通却得不到预期结构。


















