在MongoDB的聚合框架中,$merge是一个用于将管道计算结果写入目标集合的阶段。它和早期的$out不同,不会把目标集合整个清空再写入,而是根据匹配条件逐条处理,实现更新、插入或替换。这种机制特别适合需要长期累积数据、又要周期性跑聚合任务的业务。

一、$merge阶段的基本语法
$merge阶段需要至少指定目标集合名称,通常还要定义如何匹配已有文档以及匹配后如何处理。最基础的写法如下:
db.source_collection.aggregate([
{
$group: {
_id: "$user_id",
total_amount: { $sum: "$amount" }
}
},
{
$merge: {
into: "user_summary",
on: "_id",
whenMatched: "merge",
whenNotMatched: "insert"
}
}
]);
上面的例子中,源集合按用户ID分组算出总金额,结果通过$merge写入user_summary集合。on指定用_id字段做匹配,也就是用户ID;当目标里已有该用户时执行merge(合并字段),没有则插入新文档。
into除了写集合名字符串,也可以写成{ db: "other_db", coll: "user_summary" }来实现跨库写入。on可以是一个字段或多个字段组成的数组,例如on: ["order_id", "region"],这样复合键也能精确匹配。whenMatched和whenNotMatched的可选值让写入逻辑非常灵活。
二、whenMatched与whenNotMatched的详细策略
whenMatched用于控制“目标已存在匹配文档”时的动作,可选值包括replace、merge、keepExisting、fail,以及一个聚合管道数组。replace会用整个源文档替换目标文档;merge把源文档字段合并进目标(默认);keepExisting什么都不改;fail则直接报错终止。
// 使用管道数组实现只更新部分字段并追加时间戳
{
$merge: {
into: "user_summary",
on: "_id",
whenMatched: [
{ $set: { total_amount: "$$new.total_amount", updated_at: "$$NOW" } }
],
whenNotMatched: "insert"
}
}
在管道数组里,$$new代表来自上游的文档,目标原有文档用$$ROOT表示。这样就能精细控制更新内容,而不是粗暴替换。比如上面的写法只改金额和更新时间,其他历史字段原样保留。
whenNotMatched决定“没匹配到”时的行为,常用insert表示插入新文档,也可用fail让任务在遇到缺失时中断。对于报表类任务,通常选insert;对于严格对账任务,可能选fail以及时暴露数据异常。
三、$merge与$out的核心差异
很多人在初次接触时容易把$merge和$out混淆。二者都能把聚合结果落地,但$out会先删除目标集合全部文档再写入,相当于全量覆盖;而$merge是逐条比对,属于增量写入。
| 对比项 | $out | $merge |
|---|---|---|
| 写入方式 | 清空后全量写 | 按on匹配增量写 |
| 原有索引 | 保留集合但数据重建 | 完全保留 |
| 跨库写入 | 不支持 | 支持 |
| 更新粒度 | 整集合替换 | 文档级合并或替换 |
从表里可以看出,如果目标集合还承担着其他写入或查询服务,$out会导致短暂不可用且丢历史,而$merge更安全。但也正因逐条处理,$merge在超大数据量全量刷新时比$out稍慢,应依场景取舍。
四、实际应用场景示例
假设有一个订单事件集合orders,每天凌晨要把前一天的各用户消费汇总合并进user_daily表,且user_daily里已有历史日期的数据。用$merge就能只处理昨天分区并自然并入。
db.orders.aggregate([
{ $match: { created_at: { $gte: ISODate("2023-01-01"), $lt: ISODate("2023-01-02") } } },
{
$group: {
_id: { user: "$user_id", day: { $dateToString: { format: "%Y-%m-%d", date: "$created_at" } } },
sum_amount: { $sum: "$amount" }
}
},
{
$merge: {
into: "user_daily",
on: "_id",
whenMatched: "merge",
whenNotMatched: "insert"
}
}
]);
这里on用复合键_id(包含用户和日期),保证每天每用户只有一条。如果某天重跑,合并时自动更新金额,不会重复插条。相比先删后插,这种写法对线上查询影响极小。
另外一个常见用法是在聚合末尾把计算结果同步到另一个业务库做只读分析,通过into的db参数即可,无需额外导出程序。只要网络连通且账号有权限,MongoDB会直接在服务端完成跨库写。
五、使用注意事项与限制
首先,$merge阶段必须是管道最后一个阶段(除$out外不能跟其他阶段)。其次,目标集合若开启了严格模式或存在唯一索引,on字段必须能命中这些约束,否则写入会失败。另外,在分片集群中,on字段最好包含片键,否则可能触发全局广播匹配,影响性能。
// 错误示例:在分片集合上用非片键字段做on
{
$merge: {
into: "sharded_coll",
on: "email", // 若email不是片键,跨分片匹配代价高
whenNotMatched: "insert"
}
}
上述写法在分片环境可能变慢。若业务允许,可以把片键加入on数组,或改在应用层先路由再聚合。此外,$merge不支持写入固定集合(capped collection),目标必须是普通集合。
最后提醒,whenMatched设为fail时,一旦遇到重复就会中断整个聚合任务,适合数据质量校验;若用于日常报表,建议用merge或管道数组更新,避免因单条冲突导致全量任务作废。掌握这些细节后,$merge就能成为稳定可靠的数据合并工具。
MongoDBaggregation_pipelinemerge_stage修改时间:2026-08-11 21:36:33