The $merge stage writes pipeline results into a collection with upsert-like behavior—insert new rows, update matched rows, or skip them. It is the go-to stage for incremental ETL, daily summary tables, and syncing aggregated metrics without wiping the target collection.
01
into target
Which collection.
02
on fields
Match key.
03
whenMatched
Update rules.
04
whenNotMatched
Insert rules.
05
vs $out
Merge vs replace.
06
ETL patterns
Summary tables.
Fundamentals
Definition and Usage
$merge is an aggregation output stage that takes each document produced by earlier stages and writes it to a target collection. For every output document, MongoDB looks for an existing document in the target where the on fields match.
💡
Beginner tip
Think of $merge as “upsert from aggregation”: If a matching row exists → apply whenMatched (merge, replace, or custom logic). If no match → apply whenNotMatched (insert or discard). Unlike $out, the target collection is not dropped first.
Place $match and $groupbefore$merge to compute summaries, then merge them into a reporting collection. $merge must be the last stage in the pipeline.
Foundation
📝 Syntax
$merge accepts a document describing the target collection and merge rules:
into — required. Target collection name string, or { db: "analytics", coll: "summaries" } for another database.
on — optional. Field(s) to match existing documents; default is _id. Use business keys like sku or date.
whenMatched — optional. Default "merge": new fields overwrite same-named fields on the existing doc. Also: "replace", "keepExisting", "fail", or a pipeline array with access to $$new.
whenNotMatched — optional. Default "insert". Use "discard" for update-only merges or "fail" to abort on missing targets.
Terminal stage — nothing may follow $merge in the same pipeline.
Permissions — the user needs insert and update privileges on the target collection.
$group rolls up completed orders by day. $merge uses date as the match key—new dates insert, existing dates get field-level merges (e.g. updated totalRevenue).
Example 3 — Sync inventory counts by SKU
mongosh
db.inventory.aggregate([
{
$project: {
_id: 0,
sku: 1,
name: 1,
qty: 1,
warehouse: 1,
snapshotAt: "$$NOW"
}
},
{
$merge: {
into: "inventory_snapshots",
on: "sku",
whenMatched: [
{
$set: {
qty: "$$new.qty",
warehouse: "$$new.warehouse",
snapshotAt: "$$new.snapshotAt"
}
}
],
whenNotMatched: "insert"
}
}
])
// Matches on sku (not _id)
// whenMatched pipeline uses $$new for incoming fields
// Keeps name on first insert; updates qty on re-run
📤 Custom on key:
on: "sku" → business-key merge
$$new references pipeline document
3 SKUs copied to inventory_snapshots
How It Works
When the default _id match is wrong, set on to a business key. A whenMatched pipeline gives fine-grained control—update only the fields you need from $$new.
📈 Practical Patterns
Replace, update-only, and conditional merge behavior.
Example 4 — whenMatched: "replace" (full document swap)
mongosh
// Rebuild daily summary from scratch for each date key
db.orders.aggregate([
{ $match: { status: "completed" } },
{
$group: {
_id: { $dateToString: { format: "%Y-%m-%d", date: "$orderDate" } },
totalRevenue: { $sum: "$amount" },
orderCount: { $sum: 1 },
products: { $addToSet: "$product" }
}
},
{
$project: {
_id: 0,
date: "$_id",
totalRevenue: 1,
orderCount: 1,
products: 1,
rebuiltAt: "$$NOW"
}
},
{
$merge: {
into: "daily_sales_summary",
on: "date",
whenMatched: "replace",
whenNotMatched: "insert"
}
}
])
// "replace" → entire matched doc replaced by pipeline output
// Old fields not in pipeline output are removed
// vs "merge" which only overwrites same field names
📤 replace vs merge:
merge → patch fields; keep unstated fields
replace → swap whole document
Use replace when output is the full new truth
How It Works
"merge" is a field-level patch—fields absent from the pipeline doc stay on the target. "replace" substitutes the entire matched document, which is safer when your aggregation output is the complete new version.
Example 5 — whenNotMatched: "discard" (update existing only)
mongosh
// Only refresh qty for SKUs already in inventory_snapshots
// New SKUs in inventory are ignored (not inserted)
db.inventory.aggregate([
{ $match: { qty: { $lt: 200 } } },
{
$project: {
_id: 0,
sku: 1,
qty: 1,
lowStockFlag: true,
checkedAt: "$$NOW"
}
},
{
$merge: {
into: "inventory_snapshots",
on: "sku",
whenMatched: "merge",
whenNotMatched: "discard"
}
}
])
// WGT-PRO (qty 120) and LMP-DESK (qty 85) match $match
// WGT-LITE (qty 340) excluded before $merge
// SKUs not yet in inventory_snapshots are silently skipped
📤 Update-only pattern:
whenNotMatched: "discard"
No new rows created in target
Ideal for refreshing known keys only
How It Works
Combine pre-merge $match with whenNotMatched: "discard" when the target collection defines the allowed keys. Existing SKUs get updated flags; unknown SKUs never appear in the snapshot table.
Compare
📋 $merge vs related output tools
Tool / stage
Effect
Best for
$merge
Upsert pipeline docs into target by match key
Incremental ETL, summary tables, sync jobs
$out
Replace target collection entirely
Full snapshot exports, materialized views
bulkWrite()
Application-driven upserts outside aggregation
App-layer control, mixed operations
updateMany()
Update existing docs with a filter
Simple field updates without aggregation
$lookup + write
Join then write in separate step
When merge rules need app logic
🧠 How $merge Works
1
Pipeline produces docs
Earlier stages ($match, $group, $project) shape each output document.
Input
2
Lookup in target
MongoDB searches the into collection for a document where on field values equal the pipeline document’s values.
Match
3
Apply merge rules
Match found → whenMatched (merge, replace, or pipeline). No match → whenNotMatched (insert or discard).
Write
=
💾
Target collection updated
The pipeline completes with no further stages—results live in the target collection for queries and dashboards.
Important
📝 Notes
$merge must be the last stage in the aggregation pipeline.
Default match key is _id—set on when merging on business keys like date or sku.
"merge" overwrites same-named fields; unstated fields on the target document are preserved.
"replace" swaps the entire matched document—fields not in pipeline output are removed.
In whenMatched pipelines, reference the incoming document with $$new.
$merge turns aggregation into a write path: compute summaries with $group, filter with $match, then persist results incrementally without dropping the target collection.
Choose whenMatched and whenNotMatched to match your ETL rules. For full collection replacement, use $out instead.
Create a unique index on on fields in the target collection
Filter with $match before $group to reduce merge volume
Use whenNotMatched: "discard" when the target defines the allowed keys
Add lastUpdated: "$$NOW" in $project for audit trails
Test merge pipelines on a staging collection before production targets
❌ Don’t
Put stages after $merge—it is terminal
Use $merge when you need to wipe the target—use $out instead
Merge on non-unique on fields without understanding duplicate-match behavior
Forget insert/update privileges on the target collection
Assume "merge" deletes old fields—it only overwrites matching field names
Summary
Key Takeaways
Knowledge Unlocked
Five things to remember about $merge
Use these points whenever you write aggregation results to a collection.
5
Core concepts
💾01
Upsert-like
Insert or update.
Basics
🔑02
on key
Match field.
Config
🔄03
whenMatched
merge/replace.
Update
➕04
whenNotMatched
insert/discard.
Insert
🚫05
Last stage
Terminal only.
Rule
❓ Frequently Asked Questions
$merge writes aggregation pipeline results into a target collection using merge semantics—similar to an upsert. It can insert new documents when no match exists and update (merge, replace, or custom pipeline) documents when a match is found on the on fields (default _id).
$out replaces the entire target collection with pipeline output (drop-and-replace behavior). $merge upserts row-by-row: insert new docs, update matched docs, or discard rows based on whenMatched and whenNotMatched. Use $merge for incremental ETL; use $out for full snapshot exports.
whenMatched controls behavior when a pipeline document matches an existing target document: merge (default), replace, keepExisting, fail, or a custom pipeline using $$new. whenNotMatched controls non-matches: insert (default), discard, or fail.
Yes. $merge is a terminal output stage—nothing can follow it in the same pipeline. The stage writes to the target collection and does not pass documents to a next stage (unlike $match or $project).
on specifies one or more fields used to find matching documents in the target collection—default is _id. For business keys like sku or orderDate, set on: ["sku"] or on: ["date", "region"]. Those fields should be unique or indexed for predictable merges.
$merge was added in MongoDB 4.2 alongside improved aggregation output options. On Atlas and modern self-hosted deployments it is widely available. Requires insert/update privileges on the target collection.
Did you know?
$merge was introduced in MongoDB 4.2 as a safer alternative to $out for incremental workloads. You can reference the incoming document as $$new inside a whenMatched pipeline—enabling conditional updates like “only raise the high-water mark if the new value is greater.” See the official $merge docs.