不能直接用 $out 做增量归档,因为 $out 会完全替换目标集合,导致数据不可读、i/o 浪费和同步延迟;应使用 $merge 实现真正增量归档。

为什么不能直接用 $out 做增量归档
因为 $out 会**完全替换目标集合**,哪怕只新增一条数据,它也会把整个归档集合清空重写。在副本集环境下,这不仅浪费 I/O 和网络带宽,还会导致归档集合在写入中途不可读,甚至触发主从同步延迟告警。
真正适合增量归档的是 $merge —— 它允许你指定冲突策略,比如只插入新文档、按字段合并更新、或跳过已存在记录。
-
$merge必须是聚合管道的最后一个阶段 - 输出集合可以与输入集合同名、同库,也可以跨库(需权限)
- 从 MongoDB 5.0 起,
$merge支持在从节点上执行读取(但写仍发往主节点) - 若目标集合不存在,
$merge会自动创建,并建立默认索引(如_id)
$merge 的 on 字段必须对应唯一标识
增量归档成败关键在于怎么判断“这条数据是否已归档过”。MongoDB 不支持类似 MySQL 的 ON DUPLICATE KEY UPDATE 语法,而是靠 on 字段匹配已有文档。
常见错误是直接用 _id:看似唯一,但如果你归档的是日志类数据(比如 IoT 设备上报),原始集合里 _id 是 ObjectId,而归档时可能想按业务时间 + 设备 ID 组合去重 —— 这时候必须提前在归档集合上建复合唯一索引,并在 $merge 中显式指定 on:
db.raw_events.aggregate([
{ $match: { ts: { $gte: ISODate("2026-09-06T00:00:00Z") } } },
{ $merge: {
into: "archived_events",
on: ["device_id", "ts"],
whenMatched: "replace",
whenNotMatched: "insert"
}
}
])
-
on字段必须是归档集合上已存在的**唯一索引字段**(否则报错) -
whenMatched: "replace"表示已有记录则全量覆盖;也可用"merge"做字段级更新(需配合let和自定义 pipeline) -
whenNotMatched: "discard"可用于“只更新不新增”的场景(如修正历史数据)
如何避免归档过程中丢失新写入的数据
副本集的 oplog 是有限长度的,如果归档任务跑得慢,或者中间断开重连,很容易漏掉在 $match 时间范围之后、但归档开始之前写入的新文档。
稳妥做法是结合变更流(watch())做双保险:
- 先用聚合 +
$merge归档一个“基线快照”(例如最近 24 小时) - 紧接着调用
db.events.watch(),监听该集合后续所有insert和update事件 - 把变更事件解析后,再走一次轻量
$merge(注意过滤掉已归档过的event_id或时间戳)
注意:watch() 管道中只能用 $match、$project、$addFields 等白名单阶段,不能嵌套完整聚合逻辑;实时归档逻辑建议放在应用层或 Change Stream Listener 中处理。
性能和权限容易被忽略的点
在副本集上运行带 $merge 的聚合,实际写操作始终由主节点完成。如果归档频率高、数据量大,主节点的写压力会明显上升。
- 务必在归档集合的
on字段上建唯一索引(如{ device_id: 1, ts: 1 }),否则$merge会退化为全表扫描比对 - 聚合前加
$match过滤原始集合,越早缩小数据集,内存和网络开销越小 - 驱动程序版本要 >= 4.7(Node.js)、4.11(Python PyMongo),否则不支持在从节点发起含
$merge的读请求 - 用户账号需同时具备源集合的
find权限和目标集合的insert、update、replace权限
最常被跳过的一步:没检查归档集合是否已有重复数据。一旦 on 字段不是严格唯一,$merge 可能静默失败或产生多条重复记录,且不会报错 —— 务必在首次归档后手动验证 db.archived_events.distinct("device_id", { ts: { $exists: true } }) 的返回长度是否符合预期。











