Node.js

MongoDB Aggregation Pipeline: A Complete Guide

Umar Nazaket
11 min read

The aggregation pipeline is MongoDB's most powerful feature, yet most developers only scratch the surface with basic $group operations. This guide covers the patterns I use daily to build complex data analysis features without leaving the database layer.

Data analytics dashboard

Complex analytics that once required separate tools can be built with aggregation pipelines

The Golden Rule: Filter First

The single most important optimization is placing $match stages as early as possible. MongoDB can use indexes on $match stages that appear at the beginning of the pipeline, but not on those that come after transformations.

// GOOD: $match first, uses index on "status" and "createdAt"
db.orders.aggregate([
  { $match: {
    status: "completed",
    createdAt: { $gte: ISODate("2026-01-01") }
  }},
  { $group: {
    _id: "$customerId",
    totalSpent: { $sum: "$amount" },
    orderCount: { $sum: 1 }
  }},
  { $sort: { totalSpent: -1 } },
  { $limit: 20 }
]);

// BAD: $group first, scans entire collection
db.orders.aggregate([
  { $group: { _id: "$customerId", totalSpent: { $sum: "$amount" } } },
  { $match: { totalSpent: { $gte: 1000 } } }
]);

Advanced Pattern: $facet for Parallel Aggregation

The $facet stage lets you run multiple aggregation pipelines in parallel on the same input documents. This is incredibly useful for dashboard APIs where you need multiple metrics from the same dataset:

db.orders.aggregate([
  { $match: { createdAt: { $gte: ISODate("2026-01-01") } } },
  { $facet: {
    "totalRevenue": [
      { $group: { _id: null, total: { $sum: "$amount" } } }
    ],
    "byCategory": [
      { $group: { _id: "$category", revenue: { $sum: "$amount" } } },
      { $sort: { revenue: -1 } }
    ],
    "monthlyTrend": [
      { $group: {
        _id: { $month: "$createdAt" },
        revenue: { $sum: "$amount" },
        count: { $sum: 1 }
      }},
      { $sort: { "_id": 1 } }
    ]
  }}
]);

One query, three different analytical views. Without $facet, this would require three separate queries.

Data processing and analysis

$facet enables multiple analytical views from a single query execution

Materialized Views with $merge

For expensive aggregations that power dashboards, running them on every request is wasteful. The $merge stage writes aggregation results to a collection, creating a materialized view:

// Run on a schedule (e.g., every hour via cron)
db.orders.aggregate([
  { $group: {
    _id: { date: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } } },
    revenue: { $sum: "$amount" },
    orderCount: { $sum: 1 },
    avgOrderValue: { $avg: "$amount" }
  }},
  { $merge: {
    into: "daily_revenue_summary",
    on: "_id",
    whenMatched: "replace",
    whenNotMatched: "insert"
  }}
]);

Your dashboard API then reads from daily_revenue_summary — a simple, fast, indexed collection — instead of running the full aggregation pipeline on every request.

Performance Checklist

  • $match first — Always filter before transforming to leverage indexes
  • $project early — Drop unneeded fields to reduce memory usage in the pipeline
  • Use explain() — Check if your pipeline uses indexes with .explain("executionStats")
  • allowDiskUse — For large datasets, enable disk-based processing to avoid 100MB memory limit
  • $merge for caching — Pre-compute expensive aggregations on a schedule
The aggregation pipeline is not just a query tool — it is a complete data processing engine. Master it, and you can replace significant chunks of application-level data processing with efficient, index-aware database operations.
Umar Nazaket

Written by Umar Nazaket

Senior Full Stack Developer with 6+ years of Angular, React and Node.js work for healthcare, automotive, e-learning and SaaS companies.

Browse all articles Get in Touch