最可靠的做法是先用 $unwind 拆开数组再 $group,因为 MongoDB 的 $group 不会自动遍历数组元素,必须摊平后才能按数组内对象字段(如 user.id)准确分组或去重。

用 $unwind 拆开数组再 $group 是最可靠的做法
直接对包含对象的数组字段做 $group 会失败或分组不准——MongoDB 的 $group 不会自动遍历数组元素,更不会按数组内对象的某个键(比如 user.id)提取值去重。必须先用 $unwind 把数组“摊平”,让每个文档对应数组里的一个子对象。
常见错误是漏掉 $unwind 或误以为 $addToSet 能直接处理嵌套结构。实际上 $addToSet: "$items.user.id" 只能收集整个数组里所有 user.id 值,但无法保证原始文档粒度上的去重逻辑(比如“每个用户只算一次订单”这种需求)。
实操建议:
- 始终在
$unwind后加preserveNullAndEmptyArrays: false(默认值),避免空数组或缺失字段产生 null 文档干扰分组 - 如果数组字段名是
orders,且想按其中每个orders.customer.id分组,$unwind阶段写成{ $unwind: "$orders" },再在后续阶段用$orders.customer.id - 若需保留原始文档其他字段(如创建时间、状态),记得在
$group阶段用$first或$min显式提取,否则会被丢弃
$group 阶段必须用 _id 引用展开后的字段路径
很多人卡在 $group 的 _id 写法上:不能写成 _id: "$customer.id"(除非顶层就有该字段),而要严格匹配 $unwind 后的结构。比如 $unwind: "$items" 后,items.name 才是有效路径。
示例场景:集合中每个文档含 tags: [{name: "web", type: "tech"}, {name: "mobile", type: "tech"}],想按 tags.name 统计出现次数:
[
{ $unwind: "$tags" },
{ $group: { _id: "$tags.name", count: { $sum: 1 } } }
]
注意:_id: "$tags.name" 中的 tags 是 $unwind 生成的字段名,不是原始数组名本身;如果 $unwind 用了 path 别名(如 { path: "$tags", preserveNullAndEmptyArrays: true }),仍用原字段名引用。
去重而非计数?用 $addToSet 收集唯一值,但别指望它替代分组逻辑
如果你的目标不是“每个属性值一行”,而是“从所有数组对象中提取不重复的 user.id 列表”,那确实可以用 $group 配合 $addToSet,但这是聚合结果的去重,不是按该属性分组文档。
例如:想得到全部不重复的 authorId,不管它来自哪个文档:
[
{ $unwind: "$comments" },
{ $group: { _id: null, uniqueAuthors: { $addToSet: "$comments.authorId" } } }
]
这里 _id: null 表示全局归为一组;若漏掉 $unwind,$addToSet: "$comments.authorId" 会尝试把整个 comments 数组当一个值塞进去,结果不是你想要的 ID 列表。
容易踩的坑:
-
$addToSet只作用于当前$group分组内的值,无法跨组去重 - 如果数组里有重复
authorId(同一文档内多个评论是同一人),$unwind + $addToSet自然能去重;但若想“每个用户只取其最早一条评论”,就得换用$first配合排序,而不是依赖$addToSet
性能和内存要注意:大数组 + 多文档时 $unwind 可能爆炸
$unwind 本质是笛卡尔展开:一个含 100 个元素的数组,会让单文档变成 100 个。千万级文档 × 平均 10 个数组项 = 上亿中间文档,极易触发 Memory limit exceeded 错误(尤其在 32MB 默认内存限制下)。
缓解办法:
- 前置
$match尽量缩小输入文档集,比如先过滤出最近 7 天的数据 - 用
$size判断数组长度,跳过超长数组(如{ $expr: { $lt: [{ $size: "$items" }, 50] } }) - 必要时改用应用层处理:查出原始数据,在 Node.js/Python 里用 Set 去重,比在数据库里硬扛更可控
真正难的不是语法,而是想清楚你要的是“按某字段值分组统计”,还是“提取所有不重复的该字段值”——前者必须 $unwind,后者有时用 distinct 更快,别一上来就写聚合管道。


















