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:

flowchart LR IN[("orders
~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).