Aggregation Stages Deep Dive
Mastering Aggregation Stages
The aggregation pipeline has over 30 stages. Knowing how to chain $match, $group, $project, $unwind, and $lookup fluently unlocks analytics that would require complex application-layer code otherwise.
$match — Filter Early
Always push $match as early as possible. When the matched field has an index, MongoDB can do an IXSCAN instead of scanning every document.
// Only active orders in the last 30 days
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
db.orders.aggregate([
{ $match: { status: "active", createdAt: { $gte: thirtyDaysAgo } } },
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } },
{ $limit: 10 }
]);$group — Accumulate Values
$group collapses many documents into fewer by grouping on the _id expression. Use accumulators like $sum, $avg, $min, $max, $push, and $addToSet.
// Per-course stats: count, avgAge, list of names
db.students.aggregate([
{
$group: {
_id: "$course",
studentCount: { $sum: 1 },
avgAge: { $avg: "$age" },
names: { $push: "$name" }
}
}
]);$project — Shape Output
$project can include/exclude fields and build computed fields using aggregation expressions — string concatenation, arithmetic, conditional logic, and more.
db.orders.aggregate([
{
$project: {
_id: 0,
customer: 1,
amount: 1,
// computed: add 18% tax
amountWithTax: { $multiply: ["$amount", 1.18] },
// conditional label
tier: {
$cond: {
if: { $gte: ["$amount", 10000] },
then: "premium",
else: "standard"
}
}
}
}
]);$unwind and $lookup Together
$unwind deconstructs an array into one document per element. Combined with $lookup you can join across collections on array keys.
// Students have array of courseIds; join to courses collection
db.students.aggregate([
{ $unwind: "$courseIds" },
{
$lookup: {
from: "courses",
localField: "courseIds",
foreignField: "_id",
as: "courseInfo"
}
},
{ $unwind: "$courseInfo" },
{
$group: {
_id: "$_id",
name: { $first: "$name" },
courses: { $push: "$courseInfo.title" }
}
}
]);Use $facet to run multiple sub-pipelines in a single aggregation pass and get different facets of the same data (e.g. totals + breakdowns) without hitting the collection twice.