I see the same pattern constantly in MERN codebases: fetch thousands of documents from MongoDB, load them into a JavaScript array, then filter, group, and sort them in application code. This works - until it doesn't. At scale, you're moving gigabytes of data across a network and burning server memory on logic the database could execute in milliseconds. The aggregation pipeline is MongoDB's answer to 'do the work where the data lives'.
# The Mental Model: A Pipeline of Stages
Think of aggregation as an assembly line. Documents enter the first stage, get transformed, pass to the next stage, get transformed again, and so on. Each stage outputs the input for the next. The order matters enormously - filtering early with `$match` means subsequent stages process fewer documents.
Always put `$match` as early as possible in your pipeline. It reduces the number of documents flowing through every subsequent stage, which is the single most impactful performance optimization in aggregation.
# The Core Stages You'll Use 90% of the Time
$match - Filter Documents
// Identical syntax to find() - use it to reduce your working set early
{ $match: { status: "completed", userId: ObjectId("64abc...") } }$group - Aggregate Data
// Group orders by userId, compute total revenue and count
{
$group: {
_id: "$userId", // group key
totalRevenue: { $sum: "$amount" },
orderCount: { $sum: 1 },
avgOrder: { $avg: "$amount" },
firstOrder: { $min: "$createdAt" },
lastOrder: { $max: "$createdAt" },
}
}$project - Shape the Output
// Control exactly which fields appear in the output
{
$project: {
_id: 0, // exclude _id
userId: "$_id", // rename field
totalRevenue: { $round: ["$totalRevenue", 2] }, // compute on output
orderCount: 1, // include as-is
}
}# Real Example: E-Commerce Dashboard Analytics
// Get top 10 customers by revenue in the last 30 days
// with their average order value - all computed inside MongoDB
const topCustomers = await Order.aggregate([
// Stage 1: Only completed orders from last 30 days
{
$match: {
status: "completed",
createdAt: {
$gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
},
},
},
// Stage 2: Group by customer
{
$group: {
_id: "$userId",
totalSpent: { $sum: "$amount" },
orderCount: { $sum: 1 },
avgOrder: { $avg: "$amount" },
},
},
// Stage 3: Sort by highest spenders first
{ $sort: { totalSpent: -1 } },
// Stage 4: Only top 10
{ $limit: 10 },
// Stage 5: Join with users collection to get names
{
$lookup: {
from: "users",
localField: "_id",
foreignField: "_id",
as: "userInfo",
},
},
// Stage 6: Flatten the joined array
{ $unwind: "$userInfo" },
// Stage 7: Shape the final output
{
$project: {
_id: 0,
name: "$userInfo.name",
email: "$userInfo.email",
totalSpent: { $round: ["$totalSpent", 2] },
orderCount: 1,
avgOrder: { $round: ["$avgOrder", 2] },
},
},
]);# $lookup: Joins in MongoDB
MongoDB is NoSQL, but sometimes you need data from multiple collections. `$lookup` performs left outer joins. Use it sparingly - if you find yourself joining five collections in every query, your schema design might need rethinking.
// Join orders with products to get full product details
{
$lookup: {
from: "products", // collection to join
localField: "productId", // field in current documents
foreignField: "_id", // field in the joined collection
as: "product", // name for the joined array
}
},
// $lookup produces an array - use $unwind to flatten it
{ $unwind: "$product" },
// Now you can access product.name, product.price, etc.# $facet: Multiple Aggregations in One Query
`$facet` runs multiple sub-pipelines on the same input documents simultaneously. Perfect for search results pages where you need both the paginated results and the total count in one round trip.
// Single query returns paginated results AND total count AND category stats
const result = await Product.aggregate([
{ $match: { category: "electronics", price: { $lt: 500 } } },
{
$facet: {
// Sub-pipeline 1: paginated results
products: [
{ $sort: { rating: -1 } },
{ $skip: 0 },
{ $limit: 20 },
{ $project: { name: 1, price: 1, rating: 1, image: 1 } },
],
// Sub-pipeline 2: total document count
totalCount: [
{ $count: "count" },
],
// Sub-pipeline 3: price range stats
priceStats: [
{ $group: {
_id: null,
min: { $min: "$price" },
max: { $max: "$price" },
avg: { $avg: "$price" },
}},
],
},
},
]);
const { products, totalCount, priceStats } = result[0];Use `explain('executionStats')` on your aggregation pipelines during development: `db.orders.explain('executionStats').aggregate([...])`. It shows which stages are slow, whether indexes are being used, and how many documents each stage processed. The most valuable debugging tool for aggregation performance.


