Updated Table of Contents
- Introduction to Advanced Aggregation
- $unwind – Deconstructing Arrays
- $lookup – Performing Joins in MongoDB
- $facet – Multi-Faceted Aggregation
- $bucket – Grouping Data into Ranges
- $filter – Filtering Arrays in Aggregation
- Real-World Example Combining These Stages
- Conclusion
Introduction to Advanced Aggregation
MongoDB’s aggregation pipeline becomes incredibly powerful when you go beyond the basics. This module covers four advanced stages that are crucial for performing complex data operations:
$unwind
: Flattens arrays.$lookup
: Performs left outer joins.$facet
: Allows parallel pipelines for diverse analysis.$bucket
: Groups data by value ranges.
Let’s explore each with examples.
$unwind – Deconstructing Arrays
The $unwind
stage breaks an array field into multiple documents, one for each element.
Syntax:
javascriptCopyEdit{ $unwind: "$arrayField" }
Example:
javascriptCopyEditdb.orders.aggregate([
{ $unwind: "$items" }
])
If an order document has an items
array:
jsonCopyEdit{
orderId: 1,
items: ["pen", "notebook", "eraser"]
}
After $unwind
, it becomes:
jsonCopyEdit{ orderId: 1, items: "pen" }
{ orderId: 1, items: "notebook" }
{ orderId: 1, items: "eraser" }
✅ Use Case: Required when calculating statistics per array item (e.g., each item sold).
$lookup – Performing Joins in MongoDB
The $lookup
stage is MongoDB’s way to perform SQL-style joins.
Syntax:
javascriptCopyEdit{
$lookup: {
from: "collectionToJoin",
localField: "fieldInCurrentCollection",
foreignField: "fieldInOtherCollection",
as: "joinedData"
}
}
Example:
javascriptCopyEditdb.orders.aggregate([
{
$lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customerDetails"
}
}
])
Each order will now include customer info in an array field customerDetails
.
✅ Pro Tip: Use $unwind
on the joined field to flatten it into a single object if each order is tied to one customer.
$facet – Multi-Faceted Aggregation
The $facet
stage allows you to run multiple aggregation pipelines in parallel and return results in one document. This is useful for analytics dashboards.
Syntax:
javascriptCopyEdit{
$facet: {
pipeline1Name: [ /* stages */ ],
pipeline2Name: [ /* stages */ ],
...
}
}
Example:
javascriptCopyEditdb.products.aggregate([
{
$facet: {
priceStats: [
{ $group: { _id: null, avgPrice: { $avg: "$price" }, maxPrice: { $max: "$price" } } }
],
categoryCount: [
{ $group: { _id: "$category", count: { $sum: 1 } } }
]
}
}
])
Returns a single document like:
jsonCopyEdit{
"priceStats": [{ "avgPrice": 35.5, "maxPrice": 100 }],
"categoryCount": [{ "_id": "Books", "count": 10 }, ...]
}
✅ Use Case: Ideal for analytics, reporting, or dashboard queries.
$bucket – Grouping Data into Ranges
The $bucket
stage groups documents based on specified boundaries of a field (like age or price).
Syntax:
javascriptCopyEdit{
$bucket: {
groupBy: "$price",
boundaries: [0, 50, 100, 150],
default: "Others",
output: {
count: { $sum: 1 },
products: { $push: "$name" }
}
}
}
Example:
javascriptCopyEditdb.products.aggregate([
{
$bucket: {
groupBy: "$price",
boundaries: [0, 50, 100],
default: "Expensive",
output: {
count: { $sum: 1 },
items: { $push: "$name" }
}
}
}
])
This groups products into price ranges: 0–49, 50–99, and the rest as "Expensive"
.
✅ Use Case: Great for histogram-style data like age brackets, price ranges, etc.
$filter – Filtering Arrays in Aggregation
The $filter
operator allows you to return only specific elements from an array that match a certain condition. It’s extremely useful when you don’t want to unwind the array, but only want relevant values retained.
Syntax:
javascriptCopyEdit{
$filter: {
input: "<arrayField>",
as: "<variableName>",
cond: { <condition expression using the variable> }
}
}
Example: Filter completed tasks only
Assume we have a tasks
array inside user documents:
jsonCopyEdit{
name: "Alice",
tasks: [
{ title: "Task 1", completed: true },
{ title: "Task 2", completed: false },
{ title: "Task 3", completed: true }
]
}
We can filter only the completed tasks using:
javascriptCopyEditdb.users.aggregate([
{
$project: {
name: 1,
completedTasks: {
$filter: {
input: "$tasks",
as: "task",
cond: { $eq: ["$$task.completed", true] }
}
}
}
}
])
Result:
jsonCopyEdit{
name: "Alice",
completedTasks: [
{ title: "Task 1", completed: true },
{ title: "Task 3", completed: true }
]
}
✅ Use Cases of $filter
:
- Show only high-rated products from an embedded reviews array
- Return users who have only completed certain badges or certifications
- Simplify array-based filtering without flattening via
$unwind
Combined Use with $lookup
+ $filter
Suppose you do a $lookup
to bring in an array of transactions for a user, but only want to keep those where amount > 100
.
javascriptCopyEdit{
$lookup: {
from: "transactions",
localField: "_id",
foreignField: "userId",
as: "allTransactions"
}
},
{
$addFields: {
highValueTransactions: {
$filter: {
input: "$allTransactions",
as: "txn",
cond: { $gt: ["$$txn.amount", 100] }
}
}
}
}
Now, each user doc contains only high-value transactions inside highValueTransactions
.
✅ Summary of $filter
Feature | Description |
---|---|
Target | Works directly on arrays inside documents |
Use Case | Selective element retention without $unwind |
Performance | Efficient when filtering in-place |
Compatible with | $lookup , $project , $addFields |
Real-World Example Combining These Stages
Suppose you want to generate a dashboard showing:
- Total sales per item
- Top customers
- Price range distribution
javascriptCopyEditdb.orders.aggregate([
{ $unwind: "$items" },
{
$lookup: {
from: "products",
localField: "items.productId",
foreignField: "_id",
as: "productDetails"
}
},
{ $unwind: "$productDetails" },
{
$facet: {
salesPerItem: [
{ $group: { _id: "$productDetails.name", totalSold: { $sum: "$items.quantity" } } }
],
topCustomers: [
{ $group: { _id: "$customerId", totalSpent: { $sum: "$items.totalPrice" } } },
{ $sort: { totalSpent: -1 } },
{ $limit: 5 }
],
priceDistribution: [
{
$bucket: {
groupBy: "$productDetails.price",
boundaries: [0, 50, 100, 150],
default: "150+",
output: { count: { $sum: 1 } }
}
}
]
}
}
])
This single aggregation query returns three different insights in one API call.
Conclusion
MongoDB’s advanced aggregation stages like $unwind
, $lookup
, $facet
, and $bucket
give you the ability to handle deeply structured data, join across collections, and build dashboards with a single pipeline. Mastering these techniques is essential for building powerful backend APIs and data-heavy applications.