Pipeline Stages
Pipeline stages are the building blocks of the aggregation framework. Each stage performs a specific transformation on the stream of documents passing through it.
$match — Filter Documents
$match filters documents using the same query syntax as find(). Place it early to leverage indexes.
$match
{ $match: { status: 'active', age: { $gte: 18 } } }$group — Group and Accumulate
$group examples
// Count documents by status
{ $group: { _id: '$status', count: { $sum: 1 } } }
// Revenue and order count by customer
{
$group: {
_id: '$customerId',
totalSpent: { $sum: '$amount' },
orderCount: { $sum: 1 },
avgOrder: { $avg: '$amount' },
firstOrder: { $min: '$createdAt' },
lastOrder: { $max: '$createdAt' },
},
}
// Group ALL documents (no grouping field)
{ $group: { _id: null, grandTotal: { $sum: '$amount' } } }$project — Shape Output
$project
{
$project: {
_id: 0, // exclude _id
name: 1, // include as-is
fullName: { $concat: ['$first', ' ', '$last'] }, // computed
discounted: { $multiply: ['$price', 0.9] },
},
}$addFields — Add Without Listing All Fields
$addFields
// Keeps all existing fields, adds/overrides specified ones
{
$addFields: {
fullName: { $concat: ['$firstName', ' ', '$lastName'] },
totalWithTax: { $multiply: ['$price', 1.2] },
isExpensive: { $gt: ['$price', 100] },
},
}$sort — Order Documents
$sort
{ $sort: { score: -1, name: 1 } } // score desc, name asc$limit and $skip
$limit and $skip
{ $limit: 10 } // first 10 documents
{ $skip: 20 } // skip 20 documents (for page 3 of 10-per-page)$unwind — Deconstruct Arrays
$unwind
// Each element of the tags array becomes a separate document
{ $unwind: '$tags' }
// Preserve documents where the array is null or missing
{ $unwind: { path: '$tags', preserveNullAndEmptyArrays: true } }$lookup — Left Outer Join
$lookup basic
{
$lookup: {
from: 'users', // collection to join
localField: 'userId', // field in current docs
foreignField: '_id', // field in 'users'
as: 'userDetails', // output array field name
},
}$count — Count Documents
$count
{ $count: 'totalOrders' }
// Output: { totalOrders: 1547 }$facet — Multiple Sub-Pipelines
$facet runs multiple independent sub-pipelines on the SAME input documents — ideal for faceted search (counts by category, price range, etc.).
$facet
{
$facet: {
byCategory: [
{ $group: { _id: '$category', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
],
byPriceRange: [
{
$bucket: {
groupBy: '$price',
boundaries: [0, 25, 50, 100, 500],
default: '500+',
output: { count: { $sum: 1 } },
},
},
],
total: [{ $count: 'n' }],
},
}$out and $merge — Write Results
$out and $merge
// Replace a collection with pipeline results
{ $out: 'monthly_summary' }
// Merge results into an existing collection
{
$merge: {
into: 'analytics_daily',
on: '_id',
whenMatched: 'merge',
whenNotMatched: 'insert',
},
}Stage Reference
Stage | Purpose | Buffers All Docs? |
|---|---|---|
$match | Filter documents | No |
$project | Shape / compute fields | No |
$addFields | Add computed fields | No |
$group | Group and accumulate | Yes |
$sort | Order documents | Yes (unless limit follows) |
$limit | Take first N | No |
$skip | Skip N documents | No |
$unwind | Flatten array field | No |
$lookup | Join from another collection | No |
$count | Count into a field | No |
$facet | Multiple sub-pipelines | Yes |
$bucket | Group into ranges | Yes |
$out | Write to collection | Yes |
$merge | Merge into collection | Yes |