█
LastWrite
  • > Curriculum
  • > Pricing
  • > For Educators
  • > About
  • > Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
LastWrite

Structured computer science lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 LastWrite. All rights reserved.
Curriculum/Programming Languages/MongoDB/Aggregation Pipeline
60 minIntermediate

Aggregation Pipeline

After this lesson, you will be able to: Use the aggregation pipeline: $match, $group, $project, $sort, $limit, $lookup, $unwind.

Aggregation pipeline is Mongo's reporting + analytics engine. Think 'SQL GROUP BY + JOIN' but as a chain of transformations. Required reading for any production Mongo work.

Prerequisites:Query Operators

Pipeline anatomy

An array of stages; each stage transforms the docs flowing through.

css
db.orders.aggregate([
{ $match: { status: "complete" } }, // filter (like WHERE)
{ $group: { // group + aggregate
_id: "$customerId",
total: { $sum: "$amount" },
count: { $sum: 1 }
}},
{ $sort: { total: -1 } }, // sort
{ $limit: 10 } // top 10 customers by spend
]);

$match + $project

Filter early; shape later.

css
db.users.aggregate([
{ $match: { active: true, age: { $gte: 18 } } },
{ $project: {
_id: 0,
name: 1,
email: 1,
city: "$address.city", // rename nested field
isAdult: { $gte: ["$age", 18] } // computed field
}}
]);
// Put $match as early as possible, Mongo can push it to indexes

$group — the workhorse

Reduce many docs to one per group.

css
// Revenue by month
db.orders.aggregate([
{ $match: { paidAt: { $exists: true } } },
{ $group: {
_id: {
year: { $year: "$paidAt" },
month: { $month: "$paidAt" }
},
revenue: { $sum: "$amount" },
count: { $sum: 1 },
avgOrder: { $avg: "$amount" }
}},
{ $sort: { "_id.year": 1, "_id.month": 1 } }
]);
// _id: null groups everything into ONE result row
db.orders.aggregate([
{ $group: { _id: null, total: { $sum: "$amount" } } }
]);

$lookup — the join

Yes, Mongo has joins (since 3.2).

css
// Orders + customer details
db.orders.aggregate([
{ $lookup: {
from: "users", // collection to join
localField: "customerId",
foreignField: "_id",
as: "customer" // result lives here
}},
{ $unwind: "$customer" }, // explode the array (1 customer per order)
{ $project: {
orderId: "$_id",
amount: 1,
customerName: "$customer.name"
}}
]);
// $lookup is correct but slower than relational joins.
// If you do many $lookups, MAYBE you needed a relational DB.

$unwind — explode arrays

One doc per array element.

css
// Schema: { _id, name, tags: ['a', 'b', 'c'] }
db.users.aggregate([
{ $unwind: "$tags" }
]);
// → { _id, name, tags: 'a' }, { _id, name, tags: 'b' }, { _id, name, tags: 'c' }
// Often paired with $group to count tag usage
db.users.aggregate([
{ $unwind: "$tags" },
{ $group: { _id: "$tags", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]);

💡 Pipeline order matters

$match before $group: filters before aggregation = less work. $project before $group: prune fields you don't need = less memory. $limit before $lookup: lookup only on the needed docs. Mongo's optimizer reorders some stages automatically, but writing them in the right order keeps it predictable.

Common mistakes

$match late in pipeline (forces full collection scan). $lookup over and over (you're working against the data model, consider relational DB). Aggregating without indexes, slow on big collections. Misreading $sum: 1 vs $sum: '$count' (the first counts docs; the second sums the existing count field).

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←Query Operators
Back to MongoDB
Indexing in MongoDB→