MongoDB $out Stage

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

What You’ll Learn

The $out stage writes all pipeline results into a collection, replacing its contents on each run. It is the standard way to build materialized views, nightly report snapshots, and filtered export collections without application-side loops.

01

into target

Collection name.

02

Full replace

Snapshot write.

03

Materialized view

Pre-computed data.

04

Cross-db out

Analytics DB.

05

vs $merge

Replace vs upsert.

06

Terminal stage

Must be last.

Definition and Usage

$out is an aggregation output stage that takes every document from the prior stages and writes them to a target collection. When the pipeline finishes, that collection contains exactly the pipeline output—previous contents are replaced.

💡
Beginner tip

Think of $out as “save query results as a new table”:
Run aggregation → results land in report_collection.
Re-run the pipeline → report_collection is fully refreshed.
For row-by-row upserts instead, use $merge.

Typical pattern: $match to filter, $group or $project to shape, then $out to persist. Dashboards and APIs can read the materialized collection directly— fast reads without re-running heavy aggregation every time.

📝 Syntax

$out accepts a collection name string or a document with database and collection:

mongosh
{ $out: <collection> }

{ $out: { db: <database>, coll: <collection> } }

Syntax Rules

  • Same database — shorthand: { $out: "active_users" } writes to a collection in the current database.
  • Cross-database — MongoDB 4.4+: { $out: { db: "analytics", coll: "reports" } }.
  • Terminal stage — must be the last stage; nothing follows $out.
  • Not same collection — cannot read from and write to the same collection in one pipeline.
  • Creates if missing — MongoDB creates the target collection if it does not exist (subject to deployment permissions).
  • No cursor to client — the aggregation returns an empty cursor; results live in the target collection.
mongosh
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: "$region",
      totalSales: { $sum: "$amount" },
      orderCount: { $sum: 1 }
    }
  },
  {
    $project: {
      _id: 0,
      region: "$_id",
      totalSales: 1,
      orderCount: 1
    }
  },
  { $out: "sales_by_region" }
])

⚡ Quick Reference

QuestionAnswer
What it doesWrites all pipeline documents to a target collection (full replace)
Same-db syntax{ $out: "collectionName" }
Cross-db syntax{ $out: { db: "name", coll: "name" } }
Pipeline positionMust be the last stage
Re-run behaviorTarget collection replaced with new output
vs $merge$out full snapshot; $merge row-level upsert
Simple out
{ $out:
  "report" }

Same database

Cross-db
{ $out: {
  db: "analytics",
  coll: "kpi"
}}

Other database

After filter
$match →
$project →
$out

Common chain

Read result
db.report
  .find()

Query target

🧰 $out Target Options

$out has a single argument—the destination. Choose the form that matches your deployment:

String collection Same DB

Writes to a collection in the database where the aggregation runs. Simplest form for materialized views in the same app database.

{ $out: "active_employees" }
{ $out: "sales_by_region" }
db + coll Cross-DB

Writes to a collection in another database—common for separating operational data from analytics or reporting databases.

{ $out: {
  db: "reporting",
  coll: "daily_kpi"
}}
Prior stages Shape first

$out has no field options—shape documents with $project, $addFields, or $group before the output stage.

{ $project: { _id: 0, name: 1 } },
{ $out: "export" }
Restrictions Rules

Cannot output to the source collection. Cannot follow $out with more stages. Requires appropriate write permissions on the target.

// Invalid:
db.orders.aggregate([
  { $out: "orders" }  // same as source
])

Examples Gallery

Sample employees and orders—filtered exports, regional sales materialized views, shaped projections, cross-database output, and verifying results.

📚 Getting Started

Employees and orders for $out labs.

Example 1 — Employees and orders collections

mongosh
db.employees.drop()
db.orders.drop()
db.active_employees.drop()
db.sales_by_region.drop()
db.employee_directory.drop()

db.employees.insertMany([
  { name: "Alice Chen",   dept: "Engineering", status: "active",   salary: 95000,  startDate: ISODate("2022-03-15") },
  { name: "Bob Martinez", dept: "Sales",       status: "active",   salary: 72000,  startDate: ISODate("2021-08-01") },
  { name: "Carol Wu",     dept: "Engineering", status: "inactive", salary: 88000,  startDate: ISODate("2020-11-20") },
  { name: "Dan Patel",    dept: "Support",     status: "active",   salary: 65000,  startDate: ISODate("2023-01-10") },
  { name: "Eve Johnson",  dept: "Sales",       status: "active",   salary: 78000,  startDate: ISODate("2022-06-30") }
])

db.orders.insertMany([
  { region: "North", product: "Widget Pro",  amount: 299, status: "completed" },
  { region: "South", product: "Widget Lite", amount: 99,  status: "completed" },
  { region: "North", product: "Desk Lamp",   amount: 65,  status: "completed" },
  { region: "East",  product: "Office Chair",amount: 450, status: "completed" },
  { region: "West",  product: "USB Hub",     amount: 45,  status: "pending"   },
  { region: "South", product: "Widget Pro",  amount: 299, status: "completed" }
])

db.employees.createIndex({ status: 1 })
db.orders.createIndex({ status: 1, region: 1 })

How It Works

Five employees (one inactive) and six orders across four regions. Target collections start empty—they will be populated by $out examples below.

Example 2 — Export active employees to a new collection

mongosh
db.employees.aggregate([
  { $match: { status: "active" } },
  { $out: "active_employees" }
])

// Pipeline writes 4 active employees to active_employees
// Carol (inactive) is excluded by $match
// Re-run anytime → active_employees fully replaced

db.active_employees.find({}, { _id: 0, name: 1, dept: 1 })
// Alice, Bob, Dan, Eve

How It Works

The simplest $out pattern: filter with $match, then write matching documents unchanged. Apps query active_employees without repeating the status filter.

Example 3 — Materialized view: sales by region

mongosh
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: "$region",
      totalSales: { $sum: "$amount" },
      orderCount: { $sum: 1 },
      topProduct: { $first: "$product" }
    }
  },
  {
    $project: {
      _id: 0,
      region: "$_id",
      totalSales: 1,
      orderCount: 1,
      topProduct: 1,
      generatedAt: "$$NOW"
    }
  },
  { $sort: { totalSales: -1 } },
  { $out: "sales_by_region" }
])

// sales_by_region now holds aggregated KPIs
// Schedule nightly → full refresh each run
// Pending West order excluded by $match

How It Works

Combine $group with $out to pre-compute dashboards. Each pipeline run replaces the entire summary—ideal when the report should always reflect current source data as a whole snapshot.

📈 Practical Patterns

Shape output, cross-database writes, and read-back verification.

Example 4 — Public directory export (project before $out)

mongosh
db.employees.aggregate([
  { $match: { status: "active" } },
  {
    $project: {
      _id: 0,
      fullName: "$name",
      department: "$dept",
      tenureYears: {
        $dateDiff: {
          startDate: "$startDate",
          endDate: "$$NOW",
          unit: "year"
        }
      }
    }
  },
  { $sort: { fullName: 1 } },
  { $out: "employee_directory" }
])

// Only safe/public fields in employee_directory
// salary omitted — not written to export collection
// Shape documents BEFORE $out (stage has no field options)

How It Works

$out writes documents as-is from the prior stage. Use $project or $addFields upstream to strip sensitive fields and rename columns before persisting.

Example 5 — Cross-database $out (analytics reporting)

mongosh
// Ensure reporting DB exists (mongosh)
use reporting
db.createCollection("regional_kpi")

use app   // back to operational database
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: "$region",
      revenue: { $sum: "$amount" },
      orders: { $sum: 1 }
    }
  },
  {
    $project: {
      _id: 0,
      region: "$_id",
      revenue: 1,
      orders: 1,
      snapshotDate: {
        $dateToString: { format: "%Y-%m-%d", date: "$$NOW" }
      }
    }
  },
  {
    $out: {
      db: "reporting",
      coll: "regional_kpi"
    }
  }
])

// Results land in reporting.regional_kpi
// Operational app DB stays read-heavy; analytics DB holds snapshots
// Requires insert privilege on reporting.regional_kpi

How It Works

When reporting collections live in a dedicated analytics database, use the { db, coll } form. The aggregation still runs against the source collection in the current database context.

🧠 How $out Works

1

Pipeline runs

$match, $group, $project, and other stages produce the final document set in memory.

Compute
2

Target prepared

MongoDB opens the target collection (creating it if needed) and prepares to receive the full result set.

Prepare
3

Atomic replace

Pipeline documents are written to the target; previous contents are replaced so the collection matches the latest run.

Write
=

Collection ready to query

Apps, BI tools, and APIs read db.targetCollection.find()—no need to re-run the aggregation on every request.

📝 Notes

  • $out must be the last stage in the aggregation pipeline.
  • You cannot write to the same collection the pipeline reads from.
  • Re-running the pipeline replaces the entire target collection—rows from prior runs that are absent in new output are removed.
  • Shape documents with $project or $group before $out; the stage itself has no field-mapping options.
  • For incremental upserts that preserve unmatched target rows, use $merge instead.
  • Previous topic: $merge. Next: $planCacheStats.

Conclusion

$out is the simplest way to persist aggregation results: filter, transform, aggregate, then write the full snapshot to a collection your apps can query directly.

Use it for materialized views and scheduled report refreshes. When you need row-level upserts instead of full replacement, switch to $merge.

💡 Best Practices

✅ Do

  • Use $match early to reduce documents before $group and $out
  • $project sensitive fields out before writing to export collections
  • Name target collections clearly: mv_sales_by_region, active_users
  • Schedule periodic re-runs (cron, Atlas trigger) for fresh materialized views
  • Verify with db.target.countDocuments() after the pipeline completes

❌ Don’t

  • Put stages after $out—it is terminal
  • Use $out when you only need to update some rows—use $merge
  • Write to the same collection you aggregate from
  • Assume the aggregation cursor returns results—query the target collection instead
  • Run heavy $out to production targets without testing on staging first

Key Takeaways

Knowledge Unlocked

Five things to remember about $out

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

5
Core concepts
📊 02

Materialized view

Pre-compute KPIs.

Pattern
📄 03

Shape first

$project before.

Security
🗃 04

Cross-db

db + coll form.

Advanced
🚫 05

Last stage

Terminal only.

Rule

❓ Frequently Asked Questions

$out writes all documents produced by the aggregation pipeline into a target collection. On re-run, the target collection is replaced with the new pipeline output—making $out ideal for full snapshot exports and materialized views where the entire result set is the source of truth.
$out replaces the entire target collection with pipeline output. $merge upserts row-by-row using match keys and whenMatched/whenNotMatched rules. Use $out when you want a complete refresh; use $merge for incremental updates that preserve unmatched rows in the target.
Yes. $out is a terminal output stage—no stages can follow it in the same pipeline. The aggregation completes by writing to the target collection rather than returning a cursor of results to the client.
No. You cannot $out into the same collection that the pipeline reads from—it would create a read/write conflict. Write to a different collection name, or use a two-step approach with a temp collection.
Yes, in MongoDB 4.4+: { $out: { db: "analytics", coll: "daily_report" } }. The user needs insert privileges on the target database and collection.
The executing user needs insert (and replace) privileges on the target collection. On sharded clusters, the target collection must exist or MongoDB creates it according to deployment rules. Test on staging before pointing at production reporting collections.
Did you know?

MongoDB Atlas supports Online Archive and scheduled triggers that can run aggregation pipelines on a timer—pairing a nightly $out job with a materialized collection gives dashboard-speed reads without hammering your operational collections. See the official $out docs.

Continue the Stages Series

Snapshot with $out, then explore query optimization with $planCacheStats.

Next: $planCacheStats →

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