Aggregation Pipeline Basics

Introduction

Aggregation processes documents through a pipeline of stages—filter, reshape, group, and sort—like a flexible report builder. Use it when find is not enough: totals by category, daily counts, or reshaping nested arrays. This chapter covers $match, $project, $group, $sort, $limit, and $unwind.

Prerequisites

Pipeline Mental Model

text
Collection → [$match] → [$project] → [$group] → [$sort] → results

Each stage transforms the stream and passes output to the next.

$match — Filter Early

Same syntax as find filter—place $match first to reduce documents:

javascript
db.orders.aggregate([
  { $match: { status: "paid", createdAt: { $gte: new Date("2026-05-01") } } }
])

Uses indexes when $match is first stage and fields are indexed.

$project — Shape Documents

Include, exclude, compute fields:

javascript
db.orders.aggregate([
  { $match: { status: "paid" } },
  {
    $project: {
      _id: 0,
      customer: 1,
      amount: 1,
      amountDouble: { $toDouble: "$amount" },
      year: { $year: "$createdAt" }
    }
  }
])

$addFields (alias $set) adds fields while keeping others—alternative to $project.

$group — Aggregations

Group by _id expression; accumulate with operators:

javascript
db.orders.aggregate([
  { $match: { status: "paid" } },
  {
    $group: {
      _id: "$customer",
      totalSales: { $sum: { $toDouble: "$amount" } },
      orderCount: { $sum: 1 },
      avgOrder: { $avg: { $toDouble: "$amount" } }
    }
  },
  { $sort: { totalSales: -1 } }
])

Common accumulators:

OperatorUse
$sumTotals, counts
$avgAverage
$min / $maxExtremes
$pushCollect values into array
$addToSetUnique values

Group by date (day):

javascript
db.orders.aggregate([
  {
    $group: {
      _id: {
        $dateToString: { format: "%Y-%m-%d", date: "$createdAt" }
      },
      revenue: { $sum: { $toDouble: "$amount" } }
    }
  },
  { $sort: { _id: 1 } }
])

SQL analogy: GROUP BY + SUMMySQL aggregation track.

$sort, $limit, $skip

javascript
db.orders.aggregate([
  { $match: { status: "paid" } },
  { $sort: { createdAt: -1 } },
  { $limit: 10 }
])

After $group, sort on aggregated fields (totalSales, etc.).

$unwind — Expand Arrays

One document per array element:

javascript
db.orders.aggregate([
  { $unwind: "$items" },
  {
    $group: {
      _id: "$items.sku",
      totalQty: { $sum: "$items.qty" }
    }
  }
])

$unwind with preserveNullAndEmptyArrays: true keeps docs with empty/missing arrays.

Chaining Example: Top Customer

find vs aggregate

Use findUse aggregate
Simple filter + sortGrouping, sums, averages
Return raw documentsReshape, computed fields
Single collection$lookup joins — chapter 13

Performance Tips

  • Put $match and $limit early when possible
  • Index fields used in initial $match
  • Avoid huge $group without prior $match
  • Use allowDiskUse: true for large sorts in ops (memory spill)

Post-Chapter Checklist

  • Pipeline with $match$group$sort
  • $project renamed/computed fields
  • $unwind on items array
  • Compared result to equivalent SQL mental model

FAQ

aggregate vs mapReduce?

mapReduce is legacy—use aggregation pipeline.

Return cursor?

db.orders.aggregate([...]) returns cursor; use toArray() in shell for small results.

Null _id in $group?

Groups null/missing into one bucket—often filter with $match first.

$count stage?

{ $count: "total" } shortcut after $match.

Event analytics project?

See Event analytics for funnel-style pipelines.