MongoDB 的聚合管道提供了一系列阶段来处理文档,其中 $out 专门负责把管道的计算结果持久化到一个新的或已有的集合中。它通常用在需要将汇总结果保存下来供后续查询、报表展示或数据分发时。$out 的执行语义比较特殊:它不是一边计算一边写入目标集合,而是先完整执行前面的阶段,生成结果集后再统一写入。这样能保证目标集合中的数据来自一次完整的聚合快照。

理解 $out 的工作方式需要先明确它的定位。$out 只能作为聚合管道的最后一个阶段使用,因为它的职责是输出最终结果。如果把它放在中间,MongoDB 会直接报错。写入目标集合时,$out 默认会执行替换操作:如果目标集合不存在,就创建它;如果已经存在,则删除原有文档并写入新结果。这种替换语义让它特别适合生成全量汇总表,比如按天重建的用户消费统计表。下面从语法、对比和实际应用几个方面展开。
$out 的基本语法与执行机制
在 MongoDB 5.0 及之后的版本中,$out 的基本语法非常简洁。它直接写在聚合管道数组的最后一项,值的格式可以是字符串或对象。字符串形式直接指定目标集合名,例如 { $out: "customer_totals" }。对象形式则可以指定输出到其他数据库:{ $out: { db: "reporting", coll: "customer_totals" } }。这种跨库输出能力适合把计算结果写入单独的报表库。
下面这段代码展示了一个典型的销售汇总场景。订单集合 orders 中有 customerId 和 amount 字段,我们希望按客户统计消费总额,并保存到 customer_totals 集合:
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$customerId", totalAmount: { $sum: "$amount" } } },
{ $out: "customer_totals" }
]);
这段管道首先用 $match 过滤已完成订单,再用 $group 按 customerId 分组求和,最后通过 $out 把结果写入 customer_totals。执行完成后,customer_totals 集合中只包含本次聚合计算出的文档,之前的内容会被清空。如果聚合过程中上游阶段抛出异常,或者写目标集合时发生错误,$out 会尽量保证目标集合不被破坏。具体来说,MongoDB 会先把结果写入临时集合,成功后再原子性地替换目标集合;如果中途失败,临时集合会被清理,原有目标集合保持不变。不过这种保护也要看具体的存储引擎和版本行为。
另一个容易忽略的点是索引。$out 写入新集合时不会自动把源集合的索引复制过来,也不会保留目标集合上原有的索引。如果目标集合原本存在并被替换,旧索引也会随集合数据一起被移除。因此在重建汇总表之后,通常需要手动为高频查询字段创建索引。例如 db.customer_totals.createIndex({ totalAmount: -1 })。如果业务依赖索引保证查询性能,记得把建索引步骤加入任务脚本。
$out 与 $merge 的对比与选择
$out 和 $merge 都负责把聚合结果写入集合,但语义完全不同。$out 是整体替换,目标集合里最终只包含本次聚合的文档;$merge 则是合并更新,可以指定冲突时的处理方式,比如更新已有文档、保留旧文档或拒绝写入。简单说,$out 更粗暴但简单,$merge 更灵活但需要更多配置。
从版本要求来看,$merge 从 MongoDB 4.2 开始提供,而 $out 在更早版本就存在。$merge 支持将结果合并到分片集合,$out 则不允许写入分片集合。如果你需要把数据写入一个已经分片的大表,并且希望按某个字段增量更新,$merge 是唯一选择。反过来,如果任务是每日重建一张小表,$out 的替换语义反而更清晰,不容易产生重复数据。
它们的差异可以用下面的表来概括:
| 对比项 | $out | $merge |
|---|---|---|
| 写入语义 | 整体替换目标集合 | 按条件合并、更新或插入 |
| 目标集合存在时 | 清空后写入新数据 | 根据 on 字段冲突策略处理 |
| 分片集合 | 不支持 | 支持 |
| 使用复杂度 | 低 | 中高 |
举个例子,假设目标集合 customer_totals 中已经有一个 customerId 为 A 的文档,本次聚合结果也包含 customerId 为 A 的新文档。$out 会先删除旧文档,再写入新文档,最终只剩新数据。而 $merge 可以配置 { on: "_id", whenMatched: "merge" },把新旧文档字段合并,保留未变化的字段。这种能力在需要增量更新宽表时非常实用。
但也要注意,$merge 在分片集合上使用时,on 字段必须包含分片键,否则会报错。$out 由于不支持分片集合,不需要考虑这个问题。选择哪个操作符,取决于目标集合的规模、更新频率以及是否需要保留历史字段。
使用 $out 的实际应用场景
第一个典型场景是报表汇总。业务系统通常会产生大量原始订单、日志或交易记录,直接在这些大集合上做实时聚合会消耗较多计算资源。可以定期通过 $out 生成汇总表,比如每天凌晨统计前一天的销售额、用户活跃数,然后由报表系统直接读取小表。由于 $out 是整体替换,每天重建时不会产生重复数据。
第二个场景是数据归档与快照。比如每周将过期的明细数据聚合后输出到月度统计集合,再删除或归档原始明细。这样既能保留关键指标,又能控制主集合的体积。$out 跨库输出的能力允许把归档结果直接写入备份库,例如 { $out: { db: "archive", coll: "order_stats_2024_12" } },减少主库压力。
第三个场景是数据迁移或转储。有时需要把某个查询结果固定下来,供其他系统导入。$out 可以直接把聚合结果写成新集合,再通过导出工具读取该集合,比每次重新算一遍更直观。但它不适合增量同步,因为目标集合会被完全替换,无法保留每次新增的差异。
在这些场景中,最好把 $out 放到调度任务中执行,并配合 allowDiskUse: true。当聚合结果超过 100 MB 内存限制时,如果不开启磁盘临时存储,操作会失败。例如:
db.orders.aggregate(
[
{ $sort: { orderDate: -1 } },
{ $group: { _id: "$customerId", lastOrderDate: { $max: "$orderDate" } } },
{ $out: "customer_last_order" }
],
{ allowDiskUse: true }
);
这里展示了带 allowDiskUse 选项的写法。聚合选项作为 aggregate 方法的第二个参数传入,和管道阶段分开。如果省略该选项,大数据量时可能遇到内存错误。
使用 $out 时的限制与常见错误
第一类限制来自集合类型。$out 不能写入固定集合,也不能写入分片集合。固定集合有固定大小和插入顺序,替换语义无法满足其约束;分片集合的写入需要路由元数据,$out 的替换实现无法安全处理。遇到这两类目标集合,应该换用 $merge,或者先把结果写到普通集合,再手动迁移。
第二类限制与事务有关。在 MongoDB 事务中不允许使用 $out。因为 $out 会创建或替换集合,而事务中不能执行会导致集合结构变化的操作。如果管道需要在事务里运行,应避免包含 $out。
第三类问题是权限。执行 $out 的用户不仅需要对源集合有读权限,还需要对目标数据库有 createCollection、dropCollection 以及目标集合的读写权限。很多开发者只授予了源库读权限,导致聚合失败。遇到授权错误时,先检查目标库的角色是否包含这些操作。
还有一个常见的误用是把 $out 放在管道中间。例如想在过滤后先存一份中间结果,再继续聚合。这是不支持的。正确的做法是拆成两个独立的聚合任务,或者使用 $lookup、$unionWith 等阶段组合数据,而不是依赖 $out 输出中间集合。如果确实需要临时集合,可以考虑使用 $merge 或直接通过脚本控制执行顺序。
最后要提醒,$out 写入的文档是上游阶段的输出,字段顺序和类型可能因为不同版本驱动而有所差异。如果下游系统对字段顺序敏感,最好在管道末尾使用 $project 明确字段结构。例如 { $project: { _id: 1, customerId: "$_id", totalAmount: 1 } },这样生成的目标集合结构更可控。
MongoDB聚合管道$out输出新集合修改时间:2026-09-27 14:36:19