Aggregation Stages & Limits
Stages are the building blocks of every pipeline. Knowing when to use each stage separates slow reports from snappy dashboards.
Key aggregation stages — overview
Stages are the building blocks of every pipeline. Knowing when to use each stage separates slow reports from snappy dashboards.
Real-life example: Stages are kitchen stations — prep ($match), cook ($group), plate ($project), serve ($limit).
- $match — filter documents (put early)
- $group — aggregate by _id key
- $project — include/exclude/compute fields
- $unwind — one document per array element
- $sort — order results
- $limit / $skip — pagination
- $facet — multiple sub-pipelines in one query
- $bucket — histogram-style grouping by ranges
$match, $project, $unwind
$unwind duplicates the parent document for each array element — essential before grouping line items.
Real-life example: $unwind is exploding a combo meal into individual items so you can count how many fries were sold.
db.orders.aggregate([
{ $match: { status: "completed", total: { $gte: 500 } } },
{ $unwind: "$items" },
{
$project: {
orderId: 1,
sku: "$items.sku",
lineTotal: { $multiply: ["$items.qty", "$items.price"] },
_id: 0
}
}
])$sort, $limit, $skip
const page = 2;
const pageSize = 5;
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$userId", spend: { $sum: "$total" } } },
{ $sort: { spend: -1 } },
{ $skip: (page - 1) * pageSize },
{ $limit: pageSize }
])$facet and $bucket
$facet runs several sub-pipelines on the same input — one round trip for 'summary + detail + chart data'.
$bucket groups numeric values into ranges — price histograms, age bands, score tiers.
Real-life example: $facet is one dashboard API call returning both KPI cards and the table beneath them.
db.products.aggregate([
{ $match: { active: true } },
{
$facet: {
totals: [
{
$group: {
_id: null,
count: { $sum: 1 },
avgPrice: { $avg: "$price" }
}
}
],
byCategory: [
{ $group: { _id: "$category", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
],
priceHistogram: [
{
$bucket: {
groupBy: "$price",
boundaries: [0, 500, 1000, 2500, 5000, 100000],
default: "5000+",
output: { count: { $sum: 1 } }
}
}
]
}
}
])Pipeline limits and performance
Aggregation stages buffer documents in memory. By default, a stage may use up to 100 MB of RAM unless allowDiskUse: true lets MongoDB spill to disk.
Each document passing through a pipeline must stay under the 16 MB BSON document size limit — watch $push / $group arrays that grow without bound.
MongoDB limits pipeline complexity: maximum 1000 stages (you will never hit this), but heavy $lookup and unindexed $match on huge collections hurt in production.
Real-life example: Hitting memory limits is like cramming every invoice ever onto one desk — allowDiskUse moves overflow papers to a filing cabinet, but it is slower.
- allowDiskUse: true — for large analytics jobs (batch reports, ETL)
- Index fields used in early $match and $sort
- $limit early when you only need top N after $sort
- Atlas — use aggregation explain and Performance Advisor for slow pipelines
- 16 MB cap — $group with massive arrays may fail; pre-aggregate or $limit inside $group
db.events.aggregate(
[
{ $match: { ts: { $gte: ISODate("2024-01-01") } } },
{ $group: { _id: "$userId", events: { $push: "$type" } } }
],
{ allowDiskUse: true }
)