$addtoset 不能直接去重,因其仅为 $group 阶段内累加器,须配合 $unwind 才能对嵌套数组元素逐项去重,且不支持跨数组合并去重,此时应使用 $setunion。

为什么 $addToSet 在聚合管道里不能直接去重?
$addToSet 本身不是聚合阶段,而是 $group 或 $push 配合使用的累加器操作符。它只在 $group 阶段内部生效,且仅对数组字段做“添加前检查是否已存在”的动作——但它不改变输入文档结构,也不单独完成“对任意字段去重”这种需求。
常见误解是写成:{$addToSet: "$tags"} 却没放在 $group 里,结果报错 Unrecognized expression '$addToSet'。
-
$addToSet必须作为$group阶段中某个字段的值,例如{_id: null, uniqueTags: {$addToSet: "$tags"}} - 它只对单个字段(如字符串、ObjectId)或整个子文档去重;若
$tags是数组,$addToSet: "$tags"会把整个数组当一个元素塞进去,不是展开后逐项去重 - 如果想对嵌套数组里的元素去重(比如
comments.authorId),得先用$unwind拆开,再$group+$addToSet
对数组字段逐项去重:必须配合 $unwind
假设集合里每条文档有个 categories 字段,类型是字符串数组:["tech", "web", "tech"],你想提取所有不重复的 category 值。
错误写法:{$group: {_id: null, all: {$addToSet: "$categories"}}} → 得到的是 [["tech","web","tech"]],不是你想要的扁平去重结果。
正确流程是:
- 先
$unwind把$categories拆成多条文档,每条带一个字符串 - 再
$group,用$addToSet收集这些字符串 - 可选:用
$sort控制顺序($addToSet不保证顺序)
[
{"$unwind": "$categories"},
{"$group": {_id: null, uniqueCategories: {"$addToSet": "$categories"}}},
{"$project": {_id: 0, uniqueCategories: 1}}
]
$addToSet 和 $push 去重效果对比
两者都只能在 $group 中使用,但行为差异直接影响结果是否符合预期:
-
$push: "$field"无条件追加,结果是含重复项的数组 -
$addToSet: "$field"仅当值不存在时才添加,结果是去重数组 - 注意:对对象字段去重时,MongoDB 判定“相等”要求键名、键序、值完全一致;
{a:1,b:2}和{b:2,a:1}被视为不同元素 - 性能上,
$addToSet内部需查重,大数据量$group时比$push略慢,但通常可忽略
如果你已经用了 $push 得到重复数组,后续无法用聚合再“二次去重”——只能靠应用层处理或重新聚合。
替代方案:$setUnion 更适合已有数组的合并去重
当你的数据已经是多个数组(比如从不同文档收集来的标签列表),想合并并去重,$addToSet 就不适用了——它只接受标量或单个字段引用。
此时该用 $setUnion,它专为数组设计:
- 接受多个数组表达式,返回并集(自动去重)
- 可用于
$project阶段,无需$group - 示例:
{$project: {merged: {$setUnion: ["$arr1", "$arr2", ["hardcoded"]]}}} - 注意:
$setUnion要求所有输入都是数组;若字段可能为null或缺失,得先用$ifNull或$cond补空数组[]
真正容易被忽略的是:$addToSet 解决不了“已有数组之间的去重合并”,这时候硬套只会绕远路。











