MongoDBPipeline Stages

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

JS
{ $match: { status: 'active', age: { $gte: 18 } } }
Note
$match that uses a text index must be the FIRST stage in the pipeline.
$group — Group and Accumulate

$group examples

JS
// 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

JS
{
  $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

JS
// Keeps all existing fields, adds/overrides specified ones
{
  $addFields: {
    fullName:      { $concat: ['$firstName', ' ', '$lastName'] },
    totalWithTax:  { $multiply: ['$price', 1.2] },
    isExpensive:   { $gt: ['$price', 100] },
  },
}
Note
$addFields is like $project but you don't need to list every field you want to keep — only the new or modified ones.
$sort — Order Documents

$sort

JS
{ $sort: { score: -1, name: 1 } }  // score desc, name asc
Warning
$sort buffers all documents in memory. Place $limit before $sort when you only need the top-N — this lets MongoDB avoid sorting the entire result set.
$limit and $skip

$limit and $skip

JS
{ $limit: 10 }   // first 10 documents
{ $skip: 20 }    // skip 20 documents (for page 3 of 10-per-page)
$unwind — Deconstruct Arrays

$unwind

JS
// 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

JS
{
  $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

JS
{ $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

JS
{
  $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

JS
// 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

Tip
Use $addFields instead of $project when adding computed fields — you only list what is new or changed, not every field you want to keep.