Aggregation Pipeline Basics
Introduction
Aggregation processes documents through a pipeline of stages—filter, reshape, group, and sort—like a flexible report builder. Use it when find is not enough: totals by category, daily counts, or reshaping nested arrays. This chapter covers $match, $project, $group, $sort, $limit, and $unwind.
Prerequisites
- Find Queries and Projection
- Query Operators
- Indexes and explain —
$matchbenefits from indexes
Pipeline Mental Model
Collection → [$match] → [$project] → [$group] → [$sort] → resultsEach stage transforms the stream and passes output to the next.
$match — Filter Early
Same syntax as find filter—place $match first to reduce documents:
db.orders.aggregate([
{ $match: { status: "paid", createdAt: { $gte: new Date("2026-05-01") } } }
])Uses indexes when $match is first stage and fields are indexed.
$project — Shape Documents
Include, exclude, compute fields:
db.orders.aggregate([
{ $match: { status: "paid" } },
{
$project: {
_id: 0,
customer: 1,
amount: 1,
amountDouble: { $toDouble: "$amount" },
year: { $year: "$createdAt" }
}
}
])$addFields (alias $set) adds fields while keeping others—alternative to $project.
$group — Aggregations
Group by _id expression; accumulate with operators:
db.orders.aggregate([
{ $match: { status: "paid" } },
{
$group: {
_id: "$customer",
totalSales: { $sum: { $toDouble: "$amount" } },
orderCount: { $sum: 1 },
avgOrder: { $avg: { $toDouble: "$amount" } }
}
},
{ $sort: { totalSales: -1 } }
])Common accumulators:
| Operator | Use |
|---|---|
$sum | Totals, counts |
$avg | Average |
$min / $max | Extremes |
$push | Collect values into array |
$addToSet | Unique values |
Group by date (day):
db.orders.aggregate([
{
$group: {
_id: {
$dateToString: { format: "%Y-%m-%d", date: "$createdAt" }
},
revenue: { $sum: { $toDouble: "$amount" } }
}
},
{ $sort: { _id: 1 } }
])SQL analogy: GROUP BY + SUM — MySQL aggregation track.
$sort, $limit, $skip
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $sort: { createdAt: -1 } },
{ $limit: 10 }
])After $group, sort on aggregated fields (totalSales, etc.).
$unwind — Expand Arrays
One document per array element:
db.orders.aggregate([
{ $unwind: "$items" },
{
$group: {
_id: "$items.sku",
totalQty: { $sum: "$items.qty" }
}
}
])$unwind with preserveNullAndEmptyArrays: true keeps docs with empty/missing arrays.
Chaining Example: Top Customer
find vs aggregate
Use find | Use aggregate |
|---|---|
| Simple filter + sort | Grouping, sums, averages |
| Return raw documents | Reshape, computed fields |
| Single collection | $lookup joins — chapter 13 |
Performance Tips
- Put
$matchand$limitearly when possible - Index fields used in initial
$match - Avoid huge
$groupwithout prior$match - Use
allowDiskUse: truefor large sorts in ops (memory spill)
Post-Chapter Checklist
- Pipeline with
$match→$group→$sort -
$projectrenamed/computed fields -
$unwindonitemsarray - Compared result to equivalent SQL mental model
FAQ
aggregate vs mapReduce?
mapReduce is legacy—use aggregation pipeline.
Return cursor?
db.orders.aggregate([...]) returns cursor; use toArray() in shell for small results.
Null _id in $group?
Groups null/missing into one bucket—often filter with $match first.
$count stage?
{ $count: "total" } shortcut after $match.
Event analytics project?
See Event analytics for funnel-style pipelines.