$out是原子性重命名,先写临时集合再rename替换目标集合,导致索引、TTL、分片配置全丢失;$merge是文档级upsert,按on字段匹配执行replace/merge/keepExisting等策略,支持增量更新且需对应字段有索引。

$merge 和 $out 的本质区别在于:$out 是原子性重命名,$merge 是文档级 upsert 操作。 这不是“功能强弱”之分,而是底层行为完全不同 —— 一个清空重来,一个按需更新。
为什么 $out 会丢掉索引、TTL 和分片配置
$out 阶段不写入目标集合,它先在后台创建临时集合(如 esi.tmp.agg_out.2),把聚合结果全量写进去,最后用 renameCollection 命令把临时集合直接重命名为目标集合名。这个 rename 是原子操作,但也是“彻底替换”:
- 原集合的
_id索引会被保留(因为 rename 不动索引定义),但所有自定义索引、TTL 索引、分片键信息全部丢失 - 如果目标集合是分片集合,重命名后新集合默认变成非分片集合,必须手动重新分片
- 正在读取该集合的应用,在 rename 瞬间会遇到
NamespaceNotFound或短暂不可读(虽然快,但非零延迟)
$merge 如何实现“增量写入”
$merge 必须指定 on 字段(或字段数组)作为匹配依据,然后按 whenMatched 和 whenNotMatched 策略逐条处理文档:
-
whenMatched: "replace"→ 找到匹配文档就整条替换(保留原_id,但其他字段全换) -
whenMatched: "merge"→ 只合并字段(类似$set),不删未提及字段 -
whenMatched: "keepExisting"→ 匹配了也不动,跳过 -
whenNotMatched: "insert"→ 没匹配上就插入新文档(默认行为)
示例:{$merge: {into: "stats", on: "_id", whenMatched: "merge", whenNotMatched: "insert"}} —— 这才是真正的物化视图更新逻辑。
$out 和 $merge 对管道位置与兼容性的硬性要求
两者都必须是聚合管道的最后一个阶段,但错误表现和适用环境不同:
-
$out后接任何阶段(哪怕只是$limit)都会报错:"$out must be the last stage in the pipeline" -
$merge同样不能后接其他阶段,但它支持在 MongoDB 5.0+ 副本集的从节点上运行(写入仍发往主节点);$out在所有版本中都强制只在主节点执行 -
$out语法可简写为{$out: "coll"}(当前库)或{$out: {db: "db", coll: "coll"}};$merge必须用完整对象形式,且into字段不支持字符串简写
真正容易被忽略的是:$merge 的 on 字段必须有索引支撑,否则在大数据量下性能断崖式下跌 —— 它不是靠 collection scan 匹配,而是依赖索引查找,这点和 update 操作一致。


















