MongoDB Aggregation Guide
Pipeline Stage Reference
| Stage | Purpose | Notes |
|---|---|---|
| $match | Filter documents (like WHERE) | Put early to use indexes |
| $group | Group + accumulate (_id required) | $sum, $avg, $min, $max, $push, $addToSet |
| $project | Reshape output fields | 1=include, 0=exclude, expressions allowed |
| $sort | Order results | 1=asc, -1=desc; index-backed before $group |
| $limit / $skip | Pagination | Use together; $skip is expensive at scale |
| $lookup | Left outer join to another collection | Use $match pipeline variant for conditions |
| $unwind | Deconstruct array field into separate docs | preserveNullAndEmptyArrays option |
| $addFields | Add/overwrite fields without changing others | Alias: $set |
| $facet | Multiple pipelines in one pass | Good for search + count + facets |
| $bucket | Categorize into ranges | $bucketAuto for automatic ranges |
Common Aggregation Patterns
// Sales report: total revenue by category per month
db.orders.aggregate([
{ $match: { status: "completed", createdAt: { $gte: ISODate("2024-01-01") } } },
{ $unwind: "$items" },
{ $group: {
_id: { month: { $month: "$createdAt" }, category: "$items.category" },
revenue: { $sum: { $multiply: ["$items.price", "$items.qty"] } },
orderCount: { $sum: 1 }
}},
{ $sort: { "_id.month": 1, revenue: -1 } },
{ $project: {
_id: 0,
month: "$_id.month",
category: "$_id.category",
revenue: { $round: ["$revenue", 2] },
orderCount: 1
}}
]);
// $lookup with pipeline (join on multiple conditions)
db.orders.aggregate([
{ $lookup: {
from: "products",
let: { productId: "$productId", minQty: "$quantity" },
pipeline: [
{ $match: { $expr: {
$and: [
{ $eq: ["$_id", "$$productId"] },
{ $gt: ["$stock", "$$minQty"] }
]
}}},
{ $project: { name: 1, price: 1 } }
],
as: "productDetails"
}},
{ $unwind: { path: "$productDetails", preserveNullAndEmptyArrays: true } }
]);
// Faceted search (results + counts + price ranges in one query)
db.products.aggregate([
{ $match: { $text: { $search: "laptop" } } },
{ $facet: {
results: [{ $sort: { score: -1 } }, { $limit: 20 }],
totalCount: [{ $count: "count" }],
byBrand: [{ $group: { _id: "$brand", count: { $sum: 1 } } }],
priceRanges: [{ $bucket: {
groupBy: "$price",
boundaries: [0, 500, 1000, 2000, 5000],
default: "5000+",
output: { count: { $sum: 1 } }
}}]
}}
]);
Indexes for Aggregation
// Always $match early to leverage indexes
// These indexes support common aggregation patterns:
// Compound index for filtered aggregations
db.orders.createIndex({ status: 1, createdAt: -1, userId: 1 });
// Text index for $text/$search
db.products.createIndex({ name: "text", description: "text" });
// Explain aggregation pipeline
db.orders.explain("executionStats").aggregate([
{ $match: { status: "pending" } },
{ $group: { _id: "$userId", total: { $sum: "$amount" } } }
]);
// Allow disk use for large aggregations (> 100MB)
db.orders.aggregate([...], { allowDiskUse: true });