$out and $merge

Writing Results with out and merge

A pipeline usually streams its result to the client; $out and $merge write it to a collection instead. $merge is the one you will reach for, because it updates an existing collection document by document.

Incrementally maintaining a monthly rollup
db.orders.aggregate([
  { $match: { status: 'delivered', placedAt: { $lt: ISODate('2026-04-01') } } },
  { $set: { total: { $sum: { $map: { input: '$items', as: 'i',
                             in: { $multiply: ['$$i.qty', '$$i.price'] } } } } } },
  { $group: { _id: { $dateToString: { format: '%Y-%m', date: '$placedAt' } },
              revenue: { $sum: '$total' }, orders: { $sum: 1 } } },
  { $merge: { into: 'monthly', on: '_id', whenMatched: 'merge', whenNotMatched: 'insert' } }
])
db.monthly.find().sort({ _id: 1 })
Output
[
  { _id: '2026-01', revenue: 470, orders: 2 },
  { _id: '2026-02', revenue: 435, orders: 2 },
  { _id: '2026-03', revenue: 255, orders: 1 }
]

Widen the $match to the whole year and re-run: the later months are inserted and these three documents rewritten in place, which is what makes a nightly rollup cheap. on names the fields identifying a target document, _id by default; any other choice needs a unique index on those fields.

whenMatched accepts 'merge' (shallow field-by-field merge, the default), 'replace', 'keepExisting', 'fail', or an array of update stages such as [{ $set: { revenue: { $add: ['$revenue', '$$new.revenue'] } } }], where $$new is the incoming document — that last form accumulates rather than overwrites. whenNotMatched accepts 'insert' (default), 'discard' or 'fail'. 'fail' aborts on a collision, leaving documents already written in place.

$out is the blunt instrument. Ending the same pipeline with { $out: 'monthly' } writes to a temporary collection and atomically renames it over the target, so every earlier rollup vanishes. Index definitions survive the swap, but $out cannot write to a sharded collection and $merge can.