This chapter follows Indexes and query performance in MongoDB. The aggregation framework processes documents through an ordered pipeline. Each stage transforms the stream passed to the next stage.
Start with $match early to reduce working set size, then $project, $group, or $lookup as needed.
Core stages
| Stage | Role |
|---|---|
$match |
Filter (like find) |
$project |
Shape fields, compute expressions |
$group |
Aggregate by _id key |
$sort |
Order documents |
$limit / $skip |
Paginate (prefer keyset pagination for large sets) |
$unwind |
One document per array element |
import { Db } from 'mongodb';
export async function revenueByCustomer(db: Db) { return db.collection('orders').aggregate<{ _id: string; total: number }>([ { $match: { status: 'paid' } }, { $group: { _id: '$customerId', total: { $sum: '$amount' } } }, { $sort: { total: -1 } }, { $limit: 50 }, ]).toArray();}$lookup and $unwind
$lookup performs a left outer join to another collection:
[ { $lookup: { from: 'products', localField: 'items.productId', foreignField: '_id', as: 'productDetails', }}, { $unwind: '$productDetails' },]$unwind on a large array multiplies document count—filter before unwinding when possible.
$addFields and $cond
{ $addFields: { discount: { $cond: [{ $gte: ['$qty', 10] }, 0.1, 0], }, lineTotal: { $multiply: ['$price', '$qty'] }, },}$facet
Run parallel sub-pipelines in one round trip—useful for dashboard tiles:
{ $facet: { byStatus: [{ $group: { _id: '$status', count: { $sum: 1 } } }], topCustomers: [ { $group: { _id: '$customerId', total: { $sum: '$amount' } } }, { $sort: { total: -1 } }, { $limit: 5 }, ], },}$setWindowFields
Window functions compute running totals or ranks without collapsing groups:
{ $setWindowFields: { partitionBy: '$customerId', sortBy: { createdAt: 1 }, output: { runningTotal: { $sum: '$amount', window: { documents: ['unbounded', 'current'] } }, }, },}$merge, $out, and views
$outreplaces a collection with pipeline output (destructive).$mergeupserts into a target collection with configurable on fields.- Views are read-only aggregation definitions stored on the server:
db.createView('paidOrders', 'orders', [{ $match: { status: 'paid' } }])Memory and allowDiskUse
Stages buffer up to 100 MB per stage by default. Large sorts or groups may require { allowDiskUse: true } on aggregate(). Prefer $match and indexes over huge in-memory sorts.
Avoid $function and $accumulator
Server-side JavaScript in $function and $accumulator is deprecated and disabled on many Atlas tiers. Express logic with native aggregation operators ($multiply, $switch, $reduce) instead.
Next: Pagination in MongoDB.
Check your understanding
Question 1 of 7
0 of 7 checked