Photo by Eric Er on Unsplash

On this page

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
src/analytics/revenueByCustomer.ts
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

  • $out replaces a collection with pipeline output (destructive).
  • $merge upserts 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

  1. 1. Why should $match appear early in a pipeline?

Search articles

Type a word to search published articles.

Open the search page