MongoDB $merge Stage

Beginner
⏱️ 12 min read
📚 Updated: Jul 2026
🎯 5 Examples
Aggregation

What You’ll Learn

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.

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 $group before $merge to compute summaries, then merge them into a reporting collection. $merge must be the last stage in the pipeline.

📝 Syntax

$merge accepts a document describing the target collection and merge rules:

mongosh
{
  $merge: {
    into: <collection> | { db: <db>, coll: <collection> },
    on: <field | [field1, field2, ...]>,
    whenMatched: <replace | keepExisting | merge | fail | [pipeline]>,
    whenNotMatched: <insert | discard | fail>
  }
}

Syntax 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.
mongosh
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: { $dateToString: { format: "%Y-%m-%d", date: "$orderDate" } },
      totalRevenue: { $sum: "$amount" },
      orderCount: { $sum: 1 }
    }
  },
  {
    $project: {
      _id: 0,
      date: "$_id",
      totalRevenue: 1,
      orderCount: 1
    }
  },
  {
    $merge: {
      into: "daily_sales_summary",
      on: "date",
      whenMatched: "merge",
      whenNotMatched: "insert"
    }
  }
])

⚡ Quick Reference

QuestionAnswer
What it doesWrites pipeline docs to a target collection with merge/upsert rules
Default match key_id (override with on)
Default whenMatched"merge" — new fields overwrite same names
Default whenNotMatched"insert" — add new documents
Pipeline positionMust be the last stage
vs $out$merge upserts; $out replaces entire collection
Insert new
whenNotMatched:
  "insert"

Default upsert

Merge fields
whenMatched:
  "merge"

Patch update

Full replace
whenMatched:
  "replace"

Overwrite doc

Update only
whenNotMatched:
  "discard"

No inserts

🧰 $merge Options

Each option controls where data goes and how MongoDB handles matches vs non-matches:

into Required

Target collection. Same database: "daily_sales_summary". Cross-database: { db: "analytics", coll: "summaries" }.

into: "inventory_snapshots"
into: { db: "reporting", coll: "kpi" }
on Match key

Field or compound key used to find existing documents. Must exist in pipeline output. Create a unique index on on fields in production.

on: "date"
on: ["sku", "warehouseId"]
whenMatched On hit

What to do when a target document matches. String keywords or a pipeline that can reference the incoming doc as $$new.

whenMatched: "merge"
whenMatched: "replace"
whenMatched: [
  { $set: { updatedAt: "$$NOW" } }
]
whenNotMatched On miss

What to do when no target document matches. "insert" adds the pipeline document; "discard" skips it silently.

whenNotMatched: "insert"
whenNotMatched: "discard"
whenNotMatched: "fail"

Examples Gallery

Sample orders and inventory collections—daily revenue summaries, SKU sync, replace vs merge, conditional updates, and update-only patterns.

📚 Getting Started

Orders and target collections for merge labs.

Example 1 — Orders and summary collections

mongosh
db.orders.drop()
db.daily_sales_summary.drop()
db.inventory.drop()
db.inventory_snapshots.drop()

db.orders.insertMany([
  { product: "Widget Pro",  amount: 299, status: "completed", orderDate: ISODate("2025-06-01T10:00:00Z") },
  { product: "Widget Lite", amount: 99,  status: "completed", orderDate: ISODate("2025-06-01T14:30:00Z") },
  { product: "Desk Lamp",   amount: 65,  status: "completed", orderDate: ISODate("2025-06-01T16:00:00Z") },
  { product: "Widget Pro",  amount: 299, status: "completed", orderDate: ISODate("2025-06-02T09:15:00Z") },
  { product: "USB Hub",     amount: 45,  status: "pending",   orderDate: ISODate("2025-06-02T11:00:00Z") },
  { product: "Office Chair",amount: 450, status: "completed", orderDate: ISODate("2025-06-02T15:45:00Z") }
])

db.inventory.insertMany([
  { sku: "WGT-PRO",  name: "Widget Pro",  qty: 120, warehouse: "A" },
  { sku: "WGT-LITE", name: "Widget Lite", qty: 340, warehouse: "A" },
  { sku: "LMP-DESK", name: "Desk Lamp",   qty: 85,  warehouse: "B" }
])

db.orders.createIndex({ status: 1, orderDate: 1 })
db.daily_sales_summary.createIndex({ date: 1 }, { unique: true })
db.inventory_snapshots.createIndex({ sku: 1 }, { unique: true })

How It Works

Six orders across two days (one pending), three inventory SKUs, and empty target collections with unique indexes on merge keys (date and sku).

Example 2 — Build daily revenue summary (insert + merge)

mongosh
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: {
        $dateToString: { format: "%Y-%m-%d", date: "$orderDate" }
      },
      totalRevenue: { $sum: "$amount" },
      orderCount: { $sum: 1 }
    }
  },
  {
    $project: {
      _id: 0,
      date: "$_id",
      totalRevenue: 1,
      orderCount: 1,
      lastUpdated: "$$NOW"
    }
  },
  {
    $merge: {
      into: "daily_sales_summary",
      on: "date",
      whenMatched: "merge",
      whenNotMatched: "insert"
    }
  }
])

// First run → inserts 2025-06-01 and 2025-06-02 rows
// Re-run after new orders → merges updated totals into same date keys
// Pending USB Hub order excluded by $match

How It Works

$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

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

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

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.

🧠 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.

📝 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.
  • Previous topic: $match. Next: $out.

Conclusion

$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.

💡 Best Practices

✅ Do

  • 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

Key Takeaways

Knowledge Unlocked

Five things to remember about $merge

Use these points whenever you write aggregation results to a collection.

5
Core concepts
🔑 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.

Continue the Stages Series

Persist incrementally with $merge, or replace entirely with $out.

Next: $out →

About the author

Mari Selvan M P
Mari Selvan M P 🔗

Developer, cloud engineer, and technical writer

  • Experience 12 years building web and cloud systems
  • Focus Full Stack Development, AWS, and Developer Education

I write practical tutorials so students and working developers can learn by doing—from databases and APIs to deployment on AWS.

5 people found this page helpful