聚合管道本身不能删除数据,其$group、$sort等阶段仅转换数据而不修改集合;安全去重需分步:先聚合识别重复项,再用deleteMany或$merge+唯一索引实现。

聚合管道本身不能删除数据
MongoDB 的 $group、$sort 等阶段只做数据转换,不修改集合。想“用聚合管道删除重复记录”,本质是两步:先用聚合找出要保留/丢弃的文档,再用结果驱动删除操作。
常见误区是直接在 aggregate() 里加 $out 或 $merge 试图覆盖原集合——这会重写整个集合,不是“删除重复”,而是“重建去重后的新集合”,且丢失索引、TTL、权限等元信息。
安全去重的三步法(推荐)
适用于中小规模集合(百万级以内),保留原始 _id 和关键字段语义:
- 用
$group按去重字段分组,取每组中_id最小(或最大)的文档作为“代表”:[{ $group: { _id: "$email", keepId: { $min: "$_id" } } }] - 用
$lookup或两次查询,拿到所有需删除的_id(即每组中除keepId外的其他_id) - 执行
deleteMany({ _id: { $in: [ ... ] } })—— 注意:必须确保_id数组长度不超过 BSON 16MB 限制,超量需分批
大集合去重必须用 $merge + 原地更新
千万级以上数据,避免内存爆掉或超时。核心是用 $merge 把去重逻辑落地到临时集合,再原子替换:
- 先建唯一索引:
db.users.createIndex({ email: 1 }, { unique: true, partialFilterExpression: { email: { $exists: true } } })—— 后续插入会自动拒绝重复 - 聚合阶段末尾接:
{ $merge: { into: "users_dedup", on: "_id", whenMatched: "keepExisting", whenNotMatched: "insert" } } - 确认无误后,用
renameCollection原子切换:db.users_dedup.renameCollection("users", true)
注意:$merge 不会自动删旧文档,必须靠唯一索引拦截冲突;whenMatched: "keepExisting" 确保已有记录不被覆盖。
容易被忽略的坑
去重逻辑依赖字段值是否“真正相等”:空字符串 "" 和 null 在 $group 中视为不同值;大小写敏感;嵌套字段需用 $ifNull 统一处理缺失值。
如果去重依据是复合字段(如 { name: "a", age: 25 }),_id 必须显式投影为对象:_id: { name: "$name", age: "$age" },不能写成 _id: "$$ROOT" —— 否则分组键变成整个文档,失去意义。
生产环境务必先在副本集 secondary 节点上测试聚合管道输出,确认 $group 分组数量与预期一致,再执行删除。

















