R
Rishtaara
MongoDB Fundamentals
Lesson 26 of 40Article20 min

Aggregation Stages & Limits

Stages are the building blocks of every pipeline. Knowing when to use each stage separates slow reports from snappy dashboards.

Key aggregation stages — overview

Stages are the building blocks of every pipeline. Knowing when to use each stage separates slow reports from snappy dashboards.

Real-life example: Stages are kitchen stations — prep ($match), cook ($group), plate ($project), serve ($limit).

  • $match — filter documents (put early)
  • $group — aggregate by _id key
  • $project — include/exclude/compute fields
  • $unwind — one document per array element
  • $sort — order results
  • $limit / $skip — pagination
  • $facet — multiple sub-pipelines in one query
  • $bucket — histogram-style grouping by ranges

$match, $project, $unwind

$unwind duplicates the parent document for each array element — essential before grouping line items.

Real-life example: $unwind is exploding a combo meal into individual items so you can count how many fries were sold.

Filter, reshape, flatten arrays
db.orders.aggregate([
  { $match: { status: "completed", total: { $gte: 500 } } },
  { $unwind: "$items" },
  {
    $project: {
      orderId: 1,
      sku: "$items.sku",
      lineTotal: { $multiply: ["$items.qty", "$items.price"] },
      _id: 0
    }
  }
])

$sort, $limit, $skip

Large $skip values get slow — for deep pagination prefer range queries on sort keys or $search / bookmark tokens.
Top 5 customers — page 2
const page = 2;
const pageSize = 5;

db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: { _id: "$userId", spend: { $sum: "$total" } } },
  { $sort: { spend: -1 } },
  { $skip: (page - 1) * pageSize },
  { $limit: pageSize }
])

$facet and $bucket

$facet runs several sub-pipelines on the same input — one round trip for 'summary + detail + chart data'.

$bucket groups numeric values into ranges — price histograms, age bands, score tiers.

Real-life example: $facet is one dashboard API call returning both KPI cards and the table beneath them.

Dashboard in one aggregation
db.products.aggregate([
  { $match: { active: true } },
  {
    $facet: {
      totals: [
        {
          $group: {
            _id: null,
            count: { $sum: 1 },
            avgPrice: { $avg: "$price" }
          }
        }
      ],
      byCategory: [
        { $group: { _id: "$category", count: { $sum: 1 } } },
        { $sort: { count: -1 } }
      ],
      priceHistogram: [
        {
          $bucket: {
            groupBy: "$price",
            boundaries: [0, 500, 1000, 2500, 5000, 100000],
            default: "5000+",
            output: { count: { $sum: 1 } }
          }
        }
      ]
    }
  }
])

Pipeline limits and performance

Aggregation stages buffer documents in memory. By default, a stage may use up to 100 MB of RAM unless allowDiskUse: true lets MongoDB spill to disk.

Each document passing through a pipeline must stay under the 16 MB BSON document size limit — watch $push / $group arrays that grow without bound.

MongoDB limits pipeline complexity: maximum 1000 stages (you will never hit this), but heavy $lookup and unindexed $match on huge collections hurt in production.

Real-life example: Hitting memory limits is like cramming every invoice ever onto one desk — allowDiskUse moves overflow papers to a filing cabinet, but it is slower.

  • allowDiskUse: true — for large analytics jobs (batch reports, ETL)
  • Index fields used in early $match and $sort
  • $limit early when you only need top N after $sort
  • Atlas — use aggregation explain and Performance Advisor for slow pipelines
  • 16 MB cap — $group with massive arrays may fail; pre-aggregate or $limit inside $group
Run with allowDiskUse
db.events.aggregate(
  [
    { $match: { ts: { $gte: ISODate("2024-01-01") } } },
    { $group: { _id: "$userId", events: { $push: "$type" } } }
  ],
  { allowDiskUse: true }
)