CPCODELAB
MongoDB

Aggregation Stages Deep Dive

13 min read

Mastering Aggregation Stages

The aggregation pipeline has over 30 stages. Knowing how to chain $match, $group, $project, $unwind, and $lookup fluently unlocks analytics that would require complex application-layer code otherwise.

$match — Filter Early

Always push $match as early as possible. When the matched field has an index, MongoDB can do an IXSCAN instead of scanning every document.

js
// Only active orders in the last 30 days
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
db.orders.aggregate([
  { $match: { status: "active", createdAt: { $gte: thirtyDaysAgo } } },
  { $group: { _id: "$customerId", total: { $sum: "$amount" } } },
  { $sort: { total: -1 } },
  { $limit: 10 }
]);

$group — Accumulate Values

$group collapses many documents into fewer by grouping on the _id expression. Use accumulators like $sum, $avg, $min, $max, $push, and $addToSet.

js
// Per-course stats: count, avgAge, list of names
db.students.aggregate([
  {
    $group: {
      _id: "$course",
      studentCount: { $sum: 1 },
      avgAge:       { $avg: "$age" },
      names:        { $push: "$name" }
    }
  }
]);

$project — Shape Output

$project can include/exclude fields and build computed fields using aggregation expressions — string concatenation, arithmetic, conditional logic, and more.

js
db.orders.aggregate([
  {
    $project: {
      _id: 0,
      customer: 1,
      amount: 1,
      // computed: add 18% tax
      amountWithTax: { $multiply: ["$amount", 1.18] },
      // conditional label
      tier: {
        $cond: {
          if:   { $gte: ["$amount", 10000] },
          then: "premium",
          else: "standard"
        }
      }
    }
  }
]);

$unwind and $lookup Together

$unwind deconstructs an array into one document per element. Combined with $lookup you can join across collections on array keys.

js
// Students have array of courseIds; join to courses collection
db.students.aggregate([
  { $unwind: "$courseIds" },
  {
    $lookup: {
      from:         "courses",
      localField:   "courseIds",
      foreignField: "_id",
      as:           "courseInfo"
    }
  },
  { $unwind: "$courseInfo" },
  {
    $group: {
      _id: "$_id",
      name:    { $first: "$name" },
      courses: { $push: "$courseInfo.title" }
    }
  }
]);

Use $facet to run multiple sub-pipelines in a single aggregation pass and get different facets of the same data (e.g. totals + breakdowns) without hitting the collection twice.

Ready to test yourself?
Practice MongoDB— quiz & coding exercises