Aggregation Framework — Introduction
The aggregation framework is MongoDB's tool for transforming, reshaping, and summarizing data — the equivalent of SQL's GROUP BY, joins, and computed columns combined into one composable pipeline. If find() answers "which documents match," aggregation answers "what does this data look like once I filter, join, group, and reshape it."
The Pipeline Mental Model
Think of an aggregation pipeline the way you'd think of Unix pipes: each stage takes the documents produced by the previous stage as input, and passes its own output documents to the next stage. Data flows through the pipeline one stage at a time.
The Unix-pipe analogy
// Unix: cat orders.json | grep shipped | sort -k total | head -5
// MongoDB:
db.orders.aggregate([
{ $match: { status: "shipped" } }, // like grep
{ $sort: { total: -1 } }, // like sort
{ $limit: 5 } // like head
])find() vs Aggregation — When to Reach for Which
Need | Use |
|---|---|
Filter + sort + simple field selection |
|
Group and summarize (totals, averages, counts per category) | Aggregation — |
Join data from another collection | Aggregation — |
Reshape documents (rename/compute/restructure fields) | Aggregation — |
Flatten arrays into separate documents | Aggregation — |
Multiple independent result sets from one query (e.g. results + counts) | Aggregation — |
Stage Overview
Stage | Purpose |
|---|---|
$match | Filters documents — the aggregation equivalent of a find() filter |
$project | Reshapes documents — include/exclude/compute fields |
$addFields | Adds or overwrites fields while keeping everything else untouched |
$group | Groups documents by a key and computes aggregates per group |
$sort | Orders documents |
$limit / $skip | Bounds the result set |
$unwind | Flattens an array field into one document per element |
$lookup | Joins in documents from another collection |
$facet | Runs multiple sub-pipelines in parallel over the same input |
$bucket / $bucketAuto | Groups documents into ranges (histograms) |
$count | Collapses input into a single document with a count |
$out / $merge | Writes pipeline results into a collection |
A Simple Pipeline Walkthrough
Say we have an orders collection and want the total revenue per customer, for shipped orders only, highest spenders first.
Sample documents
{ _id: 1, customer: "Alice", status: "shipped", total: 42.50 }
{ _id: 2, customer: "Bob", status: "pending", total: 19.99 }
{ _id: 3, customer: "Alice", status: "shipped", total: 15.00 }
{ _id: 4, customer: "Carol", status: "shipped", total: 88.00 }Pipeline: filter → group → sort
db.orders.aggregate([
{ $match: { status: "shipped" } },
{ $group: { _id: "$customer", totalSpent: { $sum: "$total" } } },
{ $sort: { totalSpent: -1 } }
])[
{ _id: 'Carol', totalSpent: 88 },
{ _id: 'Alice', totalSpent: 57.5 }
]Notice Bob is gone entirely — his only order was pending, filtered out by the $match stage before grouping even happened. Each stage's output becomes the next stage's input, exactly like a Unix pipeline.
aggregate() Syntax
Basic call shape
db.collectionName.aggregate(
[ /* array of stage objects, executed in order */ ],
{ /* options — allowDiskUse, collation, etc. */ }
)aggregate() returns a cursor, exactly like find() — call .toArray(), .forEach(), or iterate it, rather than expecting a plain array back directly.Explaining an Aggregation
Just like a query, a pipeline can be explained to see whether early stages use an index and how many documents flow between stages.
explain() on an aggregation
db.orders.explain("executionStats").aggregate([
{ $match: { status: "shipped" } },
{ $group: { _id: "$customer", totalSpent: { $sum: "$total" } } }
])$match (and $sort, when possible) as early as possible in the pipeline. MongoDB can push a leading $match down to use an index the same way a plain find() would — filtering 10,000 documents down to 50 before an expensive $group or $lookup stage is far cheaper than filtering after.{ allowDiskUse: true } to let stages like $sort and $group spill to disk when working over very large datasets.An aggregation pipeline is an ordered array of stages, each transforming the output of the one before it.
$matchand$sortearly in the pipeline can use indexes, exactly likefind().aggregate()returns a cursor, just likefind().Use
.explain()on a pipeline the same way you would on a query, to check for missing indexes or unnecessary in-memory work.