The aggregation framework
|
This section documents the current MongoDB 8.x server line as published at the MongoDB Server Manual, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Atlas Search, Atlas Vector Search, and parts of encryption and backup) are Atlas-only — they are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official manual before being relied on in production, since MongoDB iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
The aggregation pipeline transforms and combines documents through an ordered list of stages. Each stage reads the stream of documents produced by the previous stage and emits a new stream, so a pipeline reads like a series of data transformations rather than a single declarative query.
The pipeline model
db.collection.aggregate() takes an array of stage documents. Stage keys start with $; the same stage may
appear more than once; the order is significant. The first stage reads from the collection, every later stage
reads from its predecessor’s output.
// https://www.mongodb.com/docs/manual/core/aggregation-pipeline/
db.orders.aggregate([
{ $match: { status: "shipped" } },
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } },
{ $limit: 3 }
])
{ "_id": "c-42", "total": 1806.50 }
{ "_id": "c-17", "total": 1499.99 }
{ "_id": "c-08", "total": 940.00 }
The stream of documents flows stage to stage, shrinking or reshaping as it goes:
~50k docs")] --> M["$match
status = shipped
~12k docs"] M --> G["$group
by customerId
~3k docs"] G --> S["$sort
total desc"] S --> OUT[("$merge
into top_customers")]
For single-collection reads that need no grouping or joins, the plain query API is simpler and easier to index — see Querying documents. The full list of stages is the aggregation stage reference.
Filtering and reshaping
$match filters the stream using ordinary query syntax. Place it as early as possible: a $match at the very
front of the pipeline can use a collection index.
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/
{ $match: { status: "active", age: { $gte: 21 } } }
$project chooses, drops, renames, and computes fields. $addFields (and its identical alias $set) add or
overwrite fields while keeping everything else; $unset removes fields and is the inverse of $set.
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/
{ $project: { _id: 0, name: 1, year: { $year: "$createdAt" } } }
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/set/
{ $set: { fullName: { $concat: ["$first", " ", "$last"] } } }
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/unset/
{ $unset: ["ssn", "internal.notes"] }
Grouping and accumulators
$group emits one document per distinct value of its _id expression; use _id: null to fold the whole
stream into a single document. Accumulator operators compute a value across the group: $sum, $avg,
$min, $max, $push (all values, as an array), $addToSet (distinct values), and $first / $last
(which depend on a preceding $sort).
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/
{ $group: {
_id: "$category",
count: { $sum: 1 },
revenue: { $sum: "$price" },
avgPrice:{ $avg: "$price" },
minPrice:{ $min: "$price" },
maxPrice:{ $max: "$price" },
skus: { $push: "$sku" },
brands: { $addToSet: "$brand" },
firstSku:{ $first: "$sku" },
lastSku: { $last: "$sku" }
} }
$count is shorthand for a $group that counts followed by a $project, and $sortByCount is shorthand for
grouping by an expression, counting, and sorting the counts descending.
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/count/
{ $count: "matching" }
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortByCount/
{ $sortByCount: "$category" }
Ordering, paging, and sampling
$sort, $skip, and $limit order and window the stream. A $sort directly followed by a $limit is
optimized so the engine keeps only the top N documents in memory. $sample draws a pseudo-random subset.
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/
{ $sort: { createdAt: -1 } }
{ $skip: 20 }
{ $limit: 10 }
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/sample/
{ $sample: { size: 100 } }
Working with arrays
$unwind expands an array field into one document per element. preserveNullAndEmptyArrays keeps documents
whose array is missing or empty instead of dropping them.
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/
{ $unwind: { path: "$items", preserveNullAndEmptyArrays: true } }
Joining and combining collections
$lookup performs a left outer join to another collection in the same database — the pipeline analogue of a
SQL JOIN (contrast Queries (SELECT)). The equality form matches a local
field to a foreign field:
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/
{ $lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customer"
} }
The pipeline / let form binds local values as variables ($$name) and runs an arbitrary sub-pipeline, so
the join condition can be more than equality:
{ $lookup: {
from: "prices",
let: { sku: "$sku", qty: "$qty" },
pipeline: [
{ $match: { $expr: { $and: [
{ $eq: ["$sku", "$$sku"] },
{ $lte: ["$minQty", "$$qty"] }
] } } },
{ $sort: { minQty: -1 } },
{ $limit: 1 }
],
as: "priceBreak"
} }
as is always an array; follow with $unwind when a single matched document is expected. $unionWith
concatenates another collection (optionally piped) onto the current stream:
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/
{ $unionWith: { coll: "archived_orders", pipeline: [ { $match: { year: 2023 } } ] } }
$graphLookup follows a reference recursively to resolve hierarchies and graphs (org charts, category trees,
bill-of-materials). See Data modeling for the tree/graph patterns it
pairs with.
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/graphLookup/
{ $graphLookup: {
from: "employees",
startWith: "$managerId",
connectFromField: "managerId",
connectToField: "_id",
as: "reportingChain",
maxDepth: 5
} }
Splitting and reshaping the stream
$facet runs several sub-pipelines over the same input in one pass, each producing its own array — handy
for a results page that needs data plus summary counts.
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/facet/
{ $facet: {
byStatus: [ { $group: { _id: "$status", n: { $sum: 1 } } } ],
priceStats: [ { $group: { _id: null, avg: { $avg: "$price" } } } ]
} }
$bucket groups documents into explicit ranges; $bucketAuto picks a requested number of roughly equal
buckets automatically.
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/bucket/
{ $bucket: {
groupBy: "$price",
boundaries: [0, 10, 50, 100],
default: "100+",
output: { n: { $sum: 1 }, avg: { $avg: "$price" } }
} }
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/bucketAuto/
{ $bucketAuto: { groupBy: "$price", buckets: 4 } }
$replaceRoot / $replaceWith promote an embedded document to the top level, discarding the wrapper.
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceWith/
{ $replaceWith: "$customer" }
Window functions
$setWindowFields computes values over an ordered window of documents within a partition — running totals,
moving averages, ranking — without collapsing the documents the way $group does. It is the pipeline
counterpart of SQL window functions; see
Aggregate & Window Functions.
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/
{ $setWindowFields: {
partitionBy: "$customerId",
sortBy: { orderDate: 1 },
output: {
runningTotal: { $sum: "$amount", window: { documents: ["unbounded", "current"] } },
movingAvg: { $avg: "$amount", window: { documents: [-2, 0] } }
}
} }
Writing pipeline results
$out replaces an entire target collection with the pipeline output. $merge upserts into the target,
can write to any database, and controls what happens per matched / unmatched document — the basis of
on-demand materialized views.
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/out/
{ $out: "report_snapshot" }
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/
{ $merge: {
into: "daily_rollup",
on: "_id",
whenMatched: "merge",
whenNotMatched: "insert"
} }
Expressions and system variables
Inside stages, a string beginning with $ is a field path ("$price", "$customer.city"); any other
string is a literal. Expressions nest freely and are documented in the
aggregation expression operator reference.
System variables are referenced with `: `ROOT is the whole input document, NOW` is the current
datetime, `CURRENT is the current object being processed, and $$REMOVE evaluates to "omit this field".
{ $project: {
original: "$$ROOT",
ageDays: { $dateDiff: { startDate: "$createdAt", endDate: "$$NOW", unit: "day" } },
tier: { $cond: [ { $gte: ["$spend", 1000] }, "gold", "$$REMOVE" ] }
} }
Operators fall into families: arithmetic ($add, $subtract, $multiply, $divide, $mod), string
($concat, $toLower, $split, $regexMatch), date ($year, $dateTrunc, $dateAdd, $dateToString),
array ($map, $filter, $reduce, $size, $slice, $in, $arrayElemAt), and conditional
($cond, $switch, $ifNull).
{ $addFields: {
label: { $switch: {
branches: [
{ case: { $eq: ["$status", "A"] }, then: "active" },
{ case: { $eq: ["$status", "P"] }, then: "pending" }
],
default: "unknown"
} },
nickname: { $ifNull: ["$nickname", "$name"] }
} }
Aggregation expressions inside find
$expr embeds an aggregation expression in a query predicate, so it works in find, in $match, in view
definitions, and in $lookup conditions. Its main use is comparing two fields of the same document.
// https://www.mongodb.com/docs/manual/reference/operator/query/expr/
db.budgets.find({ $expr: { $gt: ["$spent", "$limit"] } })
Optimization and explain
The query planner rewrites a pipeline before running it. Notable transforms: a $match (and the field
selection of a $project) is pushed ahead of $group and $sort when it is safe to do so; adjacent
$match stages are coalesced; and a $sort immediately followed by a $limit is combined so only the top N
documents are retained. A leading $match can be served by an index — once any document-transforming stage
has run, later filters cannot use one.
// https://www.mongodb.com/docs/manual/core/aggregation-pipeline-optimization/
db.orders.explain("executionStats").aggregate([
{ $match: { status: "shipped" } },
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } }
])
The explain output shows the rewritten stage list and whether an IXSCAN or a COLLSCAN feeds the first
stage.
Map-reduce is superseded
Standalone mapReduce is deprecated. Every grouping it expressed is covered by $group (with
$accumulator / $function for the rare case that genuinely needs custom JavaScript), and the pipeline
optimizes and parallelizes far better. See
the aggregation pipeline overview for the
recommended approach.
For the relational treatment of grouping, rollups, and windowing, see Aggregate & Window Functions and Queries (SELECT).