Chapter 21: MongoDB Aggregation $out
1. What does $out actually do? (Clear teacher explanation – no fluff)
$out is the final stage that:
Takes all the documents that survived the entire pipeline and writes them into a new collection (or replaces an existing one)
In simple words:
- You run a complex aggregation (filter → group → sort → add calculated fields…)
- Instead of just seeing the result in mongosh / your application → $out saves it as a real collection
- That new collection can now be queried normally, indexed, used in other pipelines, exposed via API, etc.
Key teacher sentences to remember forever:
- $out must be the very last stage in the pipeline (nothing can come after it)
- It completely replaces the target collection if it already exists (by default)
- It is not a view — it creates real documents on disk
- It is very useful for reporting, materialized views, pre-computed aggregates, data warehousing inside MongoDB, nightly summaries, etc.
2. Two Main Modes of $out (Important – 2026 reality)
Since MongoDB 4.2+ (and still current in 2026) there are two flavors:
| Mode / Syntax | What happens when target collection exists? | Atomic? | Use case when you choose it | Available since |
|---|---|---|---|---|
| $out: “collectionName” | Drops and recreates the collection | No | You want a fresh, complete replacement every time | 2.6 |
| $out: { db: “…”, coll: “…”, … } object | Replaces content atomically (drop+insert in one operation) | Yes | Production reporting tables – no downtime / no half-written state | 4.2+ |
The object form is almost always preferred in serious applications in 2026.
3. Hands-on Examples – Let’s Do It Step by Step
Use our familiar movieAnalytics2026 database:
|
0 1 2 3 4 5 6 |
use movieAnalytics2026 |
Example 1: Simplest – Save top-rated movies into a new collection
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
db.movies.aggregate([ { $match: { rating: { $exists: true } } }, { $sort: { rating: -1 } }, { $limit: 5 }, { $project: { title: 1, year: 1, rating: 1, genres: 1, _id: 0 } }, // The magic line – save result { $out: "topRatedMovies" } ]) |
After running → check:
|
0 1 2 3 4 5 6 7 |
show collections db.topRatedMovies.find().pretty() |
→ You now have a real collection called topRatedMovies with exactly 5 documents — permanently stored!
Example 2: Modern & recommended style (atomic replace – production favorite)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
db.orders.aggregate([ { $lookup: { from: "users", localField: "userId", foreignField: "_id", as: "customer" } }, { $unwind: { path: "$customer", preserveNullAndEmptyArrays: true } }, { $group: { _id: "$customer.city", totalAmount: { $sum: "$amount" }, orderCount: { $sum: 1 }, avgOrderValue: { $avg: "$amount" }, cities: { $addToSet: "$customer.city" } } }, { $project: { city: "$_id", totalSales: "$totalAmount", numberOfOrders: "$orderCount", averageOrder: { $round: ["$avgOrderValue", 0] }, _id: 0 } }, { $sort: { totalSales: -1 } }, // Atomic replace – safe for production { $out: { db: "shop2026", // can write to different database! coll: "citySalesSummary", timeseries: false // optional } } ]) |
Now you can query it normally:
|
0 1 2 3 4 5 6 7 |
db.citySalesSummary.find().pretty() db.citySalesSummary.createIndex({ totalSales: -1 }) // index it! |
→ This collection is now your reporting table — refreshed whenever you run the pipeline.
4. Very Important Behaviors & Gotchas (Listen carefully!)
| Point | What happens / Rule | Best Practice / Warning |
|---|---|---|
| Must be last stage | Error if anything after $out | Always put it at the very end |
| Target collection dropped first (simple mode) | Yes — data is deleted before new data is written | Dangerous in production — prefer object form |
| Atomic replace (object mode) | Drop + insert happens in one operation – no intermediate empty state | Use this in production |
| Can write to different database | Yes — { db: “reporting”, coll: “dailySummary” } | Very useful for separating operational vs analytical data |
| Indexes are NOT copied | New collection starts without indexes | Create indexes after $out if needed |
| Cannot use with $merge in same pipeline | $out and $merge are mutually exclusive | Choose one – $merge for more flexibility |
| Large result sets | Can be slow / use a lot of disk I/O | Filter early, limit if possible, use allowDiskUse: true |
5. $out vs $merge – Quick Decision Table (2026 style)
| Feature / Need | Choose $out ? | Choose $merge ? | Comment |
|---|---|---|---|
| Completely replace entire collection | Yes | Possible | $out is simpler & traditionally used for this |
| Upsert / incrementally update | No | Yes | $merge is more flexible (whenExists: “replace”, “merge”, etc.) |
| Keep existing documents not in pipeline | No | Yes | $merge can preserve unmatched documents |
| Atomic replace | Yes (object form) | Yes | Both can do it now |
| Write to different database | Yes | Yes | Both support |
| Simplicity | ★★★★★ | ★★★☆☆ | $out wins for “drop & recreate full summary” |
6. Mini Exercise – Try Right Now!
- Create a collection highRatedIndia with only Indian movies rated ≥ 8.0
- Save a summary of total revenue per genre into genreRevenueSummary
- Run the pipeline again → see that the collection is completely replaced
- Add an index on the new collection and query it normally
Understood beta? $out is the stage that turns your aggregation from a temporary calculation into a persistent, queryable asset — it’s how many production systems create fast dashboards, reporting tables, pre-joined data, daily summaries, etc.
Next class options:
- Deep comparison $out vs $merge with examples (very common interview & production question)
- How to schedule $out pipelines (Atlas Triggers, cron jobs, etc.)
- Building a complete nightly movie analytics job using $out
- Or move to $merge next?
Tell me what your heart wants next — class is still in full swing! 🚀❤️
Any confusion about $out? Ask freely — we’ll run more live examples together 😄
