A find() query answers “which documents match this filter.” An aggregation pipeline answers a different kind of question: “what does this data look like once it’s filtered, grouped, and reshaped?” It lets you filter, group, calculate totals, join collections, and reshape results through a sequence of stages — similar to combining SQL’s WHERE, GROUP BY, JOIN, and SELECT, though not a direct one-to-one equivalent of any of them.
The core idea: a pipeline of stages
Each stage takes the previous stage’s output as its input. MongoDB also optimizes pipelines automatically, so the stages as written aren’t necessarily the exact sequence of operations executed internally — but placing a selective $match early still reduces how much data later stages have to process, and an appropriate index can make that filtering more efficient.
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } },
{ $limit: 5 }
]);
Read top to bottom, this says: keep only completed orders, sum the amount per customer, sort customers by total spend descending, and keep the top 5. That’s “who are our five biggest customers by completed order value” — expressed without a single loop in application code.
The stages worth knowing first
$match— a filter, same syntax asfind(). Place it as early as the pipeline’s logic allows; an initial$matchcan use an appropriate index to reduce how many documents enter later stages.$group— combines documents sharing a key (_id) into one result per group, using accumulators such as$sum,$avg,$max,$push(collect values into an array), and$count(MongoDB 5.0+). Use$sum: 1to count documents per group when you need compatibility with older versions.$project— reshapes each document: include/exclude fields, rename them, or compute new ones.$sortand$limit— ordering and pagination, same intent as SQL’sORDER BY/LIMIT.$lookup— performs a left outer join with another collection in the same database. The matching documents are added to an array field on each input document — not flattened into individual fields automatically — which you can reshape or unwind in a later stage.
A worked example
Given an orders collection, here’s “average order value per product category, for orders in the last 30 days, highest first.” Assume each order document has a createdAt date, a numeric amount, and a category field:
// Sample input documents
{ _id: 1, category: "electronics", amount: 250, createdAt: ISODate("2026-10-15") }
{ _id: 2, category: "electronics", amount: 150, createdAt: ISODate("2026-10-18") }
{ _id: 3, category: "books", amount: 20, createdAt: ISODate("2026-10-20") }
db.orders.aggregate([
{ $match: { createdAt: { $gte: new Date(Date.now() - 30 * 86400000) } } },
{ $group: { _id: "$category", avgValue: { $avg: "$amount" }, count: { $sum: 1 } } },
{ $project: { category: "$_id", avgValue: { $round: ["$avgValue", 2] }, count: 1, _id: 0 } },
{ $sort: { avgValue: -1 } }
]);
// Output, given the sample input above
{ category: "electronics", avgValue: 200, count: 2 }
{ category: "books", avgValue: 20, count: 1 }
Each stage does one job: filter by date, group and average by category, reshape the output field names, then sort. The 30-day cutoff is computed once, client-side, when Date.now() is evaluated before the aggregation runs — fine for a one-off query like this. Debugging a pipeline that returns the wrong result almost always means running it one stage at a time — comment out everything after the suspect stage and inspect the intermediate output — rather than trying to reason about the whole pipeline at once.
When not to reach for aggregation
If the question is answerable with find() plus a filter and a sort, aggregation adds complexity for no benefit. It earns its place once the question involves grouping, computing across documents, or reshaping output — not for simple lookups.
See also: MongoDB Indexing Basics: Fixing Slow Queries for making the $match stage fast at scale, and 10 Online Code Playgrounds for React, Python & More for a hosted MongoDB sandbox to try these pipelines without a local instance.