Processing data through a sequence of staged steps like a Unix pipeline, mastering $match, $project, $group, $sort, $limit, and $unwind, and computing aggregations with $sum, $avg, $count, and $push to generate insight from raw data.

So far you've become skilled at retrieving data document by document. But modern applications don't just need raw data — they need insight: total sales per month, average rating per category, this week's best-selling products. In an RDBMS, you'd use GROUP BY and SUM. In MongoDB, you use the Aggregation Pipeline.
Episode 9 opens the aggregation phase — one of MongoDB's most powerful features. The roadmap: first we understand the concept of a pipeline that processes in stages, second we master the core $match and $project stages, third $group with accumulators, fourth $sort, $limit, $skip, and finally $unwind to open up arrays. Let's get started.
The aggregation pipeline works like Unix pipes: data flows from one stage to the next, and each stage transforms its shape. The output of one stage becomes the input of the next. The concept in action:
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $group: { _id: "$category", total: { $sum: "$total" } } },
{ $sort: { total: -1 } }
])Read the flow: take only orders with a paid status → group by category while summing the total → sort from largest. The Unix analogy: cat file | grep paid | sort. The power of the pipeline: each stage can be combined flexibly, and MongoDB optimizes the order of stages that are safe to rearrange.
The function used is aggregate, with an array containing stages. Stages are written as objects, and document fields are referenced with a $ prefix — for example, "$total" refers to the value of the total field in the document being processed.
$match filters the documents entering the pipeline — exactly like find. This is the stage that should be placed at the beginning of the pipeline for two reasons: it reduces the number of documents the next stages must process, and if it uses an indexed field, MongoDB can start from an index scan result instead of a full scan.
db.orders.aggregate([
{ $match: { status: "paid", createdAt: { $gte: ISODate("2026-01-01") } } },
{ $count: "paidOrders2026" }
])The $count stage produces a single document containing the number of documents that passed the previous stage — a quick way to count.
$project selects which fields are carried to the next stage, similar to projection in find, but richer — it can create new computed fields:
db.orders.aggregate([
{ $project: {
orderId: "$_id",
total,
status,
totalWithTax: { $multiply: ["$total", 1.11] }
} }
])The totalWithTax field is computed as the pipeline runs — multiplying total by 1.11 to add an 11 percent tax.
$addFields adds new fields without discarding old ones — the opposite philosophy of the selective $project. It's the best stage for incremental calculations whose results are used by the next stage:
db.orders.aggregate([
{ $addFields: {
year: { $year: "$createdAt" },
month: { $month: "$createdAt" }
} },
{ $group: {
_id: { year: "$year", month: "$month" },
totalRevenue: { $sum: "$total" }
} }
])This example extracts the year and month from createdAt as new fields, then groups by both — the foundation of a monthly sales report.
$group is the most transformative stage. It groups documents by the specified _id field, then computes accumulators for each group. The accumulators used most often:
| Accumulator | Function |
|---|---|
$sum | Sums values |
$avg | Averages values |
$min / $max | Smallest / largest value |
$count | Counts documents per group |
$push | Collects values into an array |
$addToSet | Collects unique values into an array |
db.orders.aggregate([
{ $group: {
_id: "$category",
totalRevenue: { $sum: "$total" },
averageOrder: { $avg: "$total" },
maxOrder: { $max: "$total" },
minOrder: { $min: "$total" },
orderCount: { $count: {} }
} }
])Notice the $count: {} pattern inside $group — it counts the number of documents per group. The result is a complete summary per category: total revenue, average, maximum, minimum, and order count.
Just like in find, the pipeline supports sorting and limiting results — useful for showing rankings or pagination:
db.products.aggregate([
{ $sort: { soldCount: -1 } },
{ $limit: 5 }
])The query above produces the five best-selling products. The $sort before $limit pattern is an important technique: MongoDB can optimize it to use an index and avoid a full sort.
$unwind splits every document containing an array into several documents — one per array element. It's a crucial stage when data is embedded in an array and you need per-element aggregation.
{
"_id": ObjectId("66xxxxxxxxxxxxxxxxxxxxxxxx"),
"status": "paid",
"items": [
{ "name": "Laptop Gaming", "price": 15000000, "qty": 1 },
{ "name": "Mouse", "price": 150000, "qty": 2 }
]
}db.orders.aggregate([
{ $match: { status: "paid" } },
{ $unwind: "$items" },
{ $group: {
_id: "$items.name",
totalSold: { $sum: "$items.qty" },
revenue: { $sum: { $multiply: ["$items.price", "$items.qty"] } }
} },
{ $sort: { revenue: -1 } }
])Without $unwind, $group can't sum per item because the array is sealed inside the document. After $unwind, each item becomes a separate document and can be aggregated — total units sold and revenue per product.
Info
Stage order greatly determines performance. The rule of thumb: place $match and $project stages that trim data at the beginning of the pipeline, $unwind before $group if needed, and $sort/$limit to cap the output. A pipeline that selects data first is far faster than one that processes everything then throws it away. Use db.collection.explain("executionStats") — which we dissect in episode 13 — to see how many documents pass through each stage.
Warning
$unwind can drastically multiply the pipeline's size: one document with 100 array elements becomes 100 documents. If the array is huge and you only need a summary, consider computing on the application side or storing pre-aggregated data. Get into the habit of placing $match before $unwind to minimize multiplication.
In episode 9 you mastered the foundations of the aggregation pipeline: the concept of sequential stages like Unix pipes, $match to filter at the start of the pipeline, $project and $addFields to reshape documents, $group with the $sum, $avg, $count, $push, and $addToSet accumulators, $sort, $limit, $skip to order and cap output, and $unwind to open arrays into individual documents.
Key takeaways:
$match at the start of the pipeline trims data and leverages indexes.$group + accumulators are the heart of statistical aggregation.$unwind opens arrays so they can be aggregated per element.$sort then cap with $limit for efficient results.In the next episode, episode 10, we push aggregation to a professional level: Advanced Aggregation: Lookups, Facets & Window Functions. You'll combine data across collections with $lookup, run several sub-pipelines in parallel with $facet, group into buckets with $bucket, and compute rankings and running totals with $setWindowFields. See you in episode 10!