Build multi-stage aggregation pipelines with $match, $group, $lookup, $project, and $unwind.
Published April 9, 2025
The aggregation pipeline is MongoDB's answer to SQL GROUP BY, JOINs, and computed columns. Data flows through a sequence of stages, each transforming the documents.
db.orders.aggregate([
// Stage 1: filter (like WHERE)
{ $match: { status: 'COMPLETED', createdAt: { $gte: new Date('2024-01-01') } } },
// Stage 2: group (like GROUP BY + aggregate functions)
{ $group: {
_id: '$userId',
totalRevenue: { $sum: '$total' },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: '$total' },
lastOrder: { $max: '$createdAt' }
}},
// Stage 3: compute new fields
{ $addFields: {
isHighValue: { $gte: ['$totalRevenue', 1000] }
}},
// Stage 4: sort
{ $sort: { totalRevenue: -1 } },
// Stage 5: limit
{ $limit: 10 },
// Stage 6: reshape output
{ $project: {
userId: '$_id',
totalRevenue: 1,
orderCount: 1,
isHighValue: 1,
_id: 0
}}
])
db.orders.aggregate([
{ $match: { userId: 'u123' } },
// Join with users collection
{ $lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'userDetails'
}},
// $lookup produces an array — unwind to flatten
{ $unwind: '$userDetails' },
{ $project: {
orderId: '$_id',
userName: '$userDetails.name',
total: 1
}}
])
// Document: { _id: 1, tags: ['java', 'spring', 'api'] }
db.posts.aggregate([
{ $unwind: '$tags' }
])
// Produces 3 documents: one per tag
// { _id: 1, tags: 'java' }
// { _id: 1, tags: 'spring' }
// { _id: 1, tags: 'api' }
db.products.aggregate([
{ $facet: {
byCategory: [
{ $group: { _id: '$category', count: { $sum: 1 } } }
],
priceStats: [
{ $group: { _id: null, avg: { $avg: '$price' }, max: { $max: '$price' } } }
],
topProducts: [
{ $sort: { sales: -1 } },
{ $limit: 5 }
]
}}
])
$match and $limit as early as possible — reduces documents flowing through later stages$match on indexed fields — MongoDB can use the index before loading documents$project early — drop unneeded fields to reduce memory usageallowDiskUse: true for pipelines that exceed the 100MB memory limitdb.orders.aggregate([...], { allowDiskUse: true })
// Daily revenue report
db.orders.aggregate([
{ $match: { status: 'COMPLETED' } },
{ $group: {
_id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } },
revenue: { $sum: '$total' },
orders: { $sum: 1 }
}},
{ $sort: { _id: 1 } }
])
$match before $group is critical for performance — it reduces the input set.$lookup is a left outer join by default.MongoTemplate.aggregate() or @Aggregation annotation in Spring Data.