$merge is an aggregation output stage that writes pipeline results into a collection with upsert-like behavior— insert new rows, update matched rows, or skip them—without wiping the target.
Remember
$merge = "upsert pipeline results into a collection"
{
$merge: {
into: "daily_sales",
on: "date",
whenMatched: "merge",
whenNotMatched: "insert"
}
}
into → target collection
on → match key (default _id)
whenMatched → merge | replace | keepExisting | fail
whenNotMatched → insert | discard | fail
MUST be the LAST stage
vs $out → $out replaces the whole collection
Goal: group orders by date, then upsert into daily_sales on date.
Calculation
target daily_sales:
{ date: "2026-10-01", total: 100, source: "east" }
pipeline result:
{ date: "2026-10-01", total: 150 } ← matches on date
{ date: "2026-10-02", total: 80 } ← no match
whenMatched: "merge"
→ { date: "2026-10-01", total: 150, region: "east" }
whenNotMatched: "insert"
→ insert { date: "2026-10-02", total: 80 }
target keeps unrelated rows; no full collection drop
Code
MongoDB Examples
We use orders line items and a summary collection daily_sales. Seed them once in Example 1, then run each $merge pipeline. Copy each block into mongosh.
Example 1 — Sample Data
Imagine order rows for two dates plus one existing summary row for 2026-10-01 with an extra region field.
merge → keep extra target fields (e.g. region)
replace → target doc becomes exactly the pipeline doc
Use replace when the result is the full source of truth
How It Works
Same pipeline. Group and project as before.
replace. Matched target row is fully swapped.
Fields drop. Extra target fields (like region) disappear.
Still incremental. Unrelated target dates stay; unlike $out.
Example 4 — Update-Only Discard
Goal: update existing summary rows only—never insert new dates. Orders still come from Example 1; this demo resets the merge target first.
{ date: "2026-10-01", total: 150 }
// only one document — new dates were discarded
How It Works
Target has one key. Only 2026-10-01 exists.
Matched. That date merges the new total.
discard.2026-10-02 has no match → skipped.
Use case. Refresh known keys without growing the target.
Edge Cases & Pitfalls
Check these before writing production ETL pipelines.
last
Must be the last stage
Nothing may follow $merge. Put filters and projections before it.
on
Match key must exist on both sides
Pipeline docs need the on fields. Project them explicitly if $group put the key in _id.
unique
Prefer a unique index on on
Duplicate match keys make merge behavior unpredictable. Create a unique index on business keys you merge on.
out
Do not confuse with $out
$out replaces the whole collection. $merge keeps existing rows and upserts.
Analysis
Performance Notes
Approach
Cost
Best for
$match before $merge
Fewer writes
Incremental windows
Unique index on on
Faster lookups
Business-key merges
Large full refresh via $merge
Many upserts
Prefer $out for full replace
Custom whenMatched pipeline
Extra per-doc work
Complex field merges only
Write less: filter early, merge on indexed keys, and use $out when you truly want a full snapshot.
Remember
Key Takeaways
Role: upsert aggregation results into a collection.
Rules: must be last; match with on.
Modes:whenMatched / whenNotMatched control insert vs update.
vs $out: incremental upsert vs full collection replace.
One line:$merge answers “how do I write pipeline results into a collection without wiping it?”
Frequently Asked Questions
$merge writes aggregation results into a target collection with upsert-like behavior. It can insert new documents when no match exists and update matched documents using whenMatched (merge, replace, keepExisting, fail, or a custom pipeline).
$out replaces the entire target collection. $merge upserts row-by-row based on on fields and whenMatched / whenNotMatched. Use $merge for incremental ETL; use $out for full snapshot exports.
whenMatched runs when a pipeline document matches a target doc: merge (default), replace, keepExisting, fail, or a custom pipeline with $$new. whenNotMatched runs when there is no match: insert (default), discard, or fail.
Yes. $merge is a terminal output stage—nothing can follow it. It writes to the target collection and does not pass documents to a next stage.
on is the match key in the target collection—default _id. For business keys use on: "sku" or on: ["date", "region"]. Those fields should be unique (or uniquely indexed) for predictable merges.
Your user needs permission to insert and/or update documents in the target collection, depending on whenMatched and whenNotMatched.