$merge 是 mongodb 4.2+ 实现按需更新物化视图的正确方式,它避免 $out 全量覆盖风险,支持基于 _id 或自定义字段的增量合并,具备匹配逻辑区分、事务安全及变更流兼容等优势。

$merge 是 MongoDB 4.2+ 实现按需更新物化视图的正确方式,它能避免 $out 的全量覆盖风险,支持基于 _id 或自定义字段的增量合并。
为什么不能只用 $out 做物化视图
$out 阶段每次运行都会删除目标集合全部文档,再写入新结果。这意味着:
- 查询期间可能遇到空集合(写入窗口期不可用)
- 无法保留历史数据、审计字段或手动补充的元数据
- 无法监听变更流(
$out不触发 change stream 事件) - 高并发调度下容易因竞态导致数据丢失
而 $merge 明确区分匹配逻辑(whenMatched)和未匹配逻辑(whenNotMatched),天然适配“有则更新、无则插入”的物化场景。
$merge 必须指定的三个核心参数
into、on、whenMatched 缺一不可,否则会报错 "$merge stage requires 'into' field" 或 "'on' field is required":
-
into:目标集合名,可为字符串(如"sales_summary")或对象(如{db: "reporting", coll: "daily_metrics"}) -
on:用于匹配的字段名,通常为"_id";也可传数组(如["customerId", "date"])实现复合键匹配 -
whenMatched:决定已有文档如何处理,常用值是"replace"(全量替换)、"merge"(仅合并指定字段)、"keepExisting"(跳过不更新)
示例管道片段:
{$merge: {into: "monthly_sales", on: "monthKey", whenMatched: "merge", whenNotMatched: "insert"}}
注意:whenNotMatched: "insert" 是安全默认值;若设为 "discard",新数据将被丢弃,极易误删数据。
常见踩坑点:on 字段必须存在于输入文档中
如果聚合管道输出的文档不含 on 指定字段(比如你设了 on: "userId",但管道没用 $addFields 或 $set 补上该字段),$merge 会静默跳过该文档——既不报错,也不写入,最终结果缺失。
- 务必在
$merge前加一个$set阶段确保关键匹配字段存在 - 对数组展开类操作(如
$unwind)后,原始_id可能丢失,需显式保留:{$set: {originalId: "$_id"}} - 测试时可用
db.collection.aggregate([...]).toArray()先检查输出结构是否含on字段
错误示例(漏掉 $set):
[{$group: {_id: "$category", total: {$sum: "$amount"}}}, {$merge: {into: "cat_totals", on: "_id", whenMatched: "replace", whenNotMatched: "insert"}}]
→ 这里 _id 是 group 生成的,没问题;但若换成 on: "category" 就必须先 $set: {category: "$_id"}。
增量更新 vs 全量重建:选哪个取决于业务容忍度
$merge 适合高频小批量更新(如每小时跑一次销售汇总),但要注意:
- 性能开销略高于
$out(因需执行查找 + 判定 + 写入三步) - 若管道本身计算量极大(如多层
$lookup+$facet),瓶颈不在写入而在计算,此时$merge的优势更明显 - 若业务要求强一致性(如财务对账),建议配合
let和 pipeline 形式的whenMatched做条件更新,避免覆盖关键状态字段
真正容易被忽略的是:没有索引的 on 字段会让 $merge 变慢十倍以上。务必在目标集合上对 on 字段建索引,例如:
db.monthly_sales.createIndex({monthKey: 1})











