MongoDB Facets for Aggregation
Learn to use facets for multi-dimensional aggregation in MongoDB. This lesson explains how $facet creates multiple aggregations in one stage, covers hands-on exercises, troubleshooting, and next steps.
Focus: use facets for multi-dimensional aggregation
You've mastered single-purpose aggregations — filtering with $match, grouping with $group, sorting with $sort. But what happens when your dashboard needs three different summaries from the same dataset at once? Run three separate queries? That's slow, wasteful, and syncing them is a nightmare. MongoDB's $facet stage lets you execute multiple aggregation pipelines in a single pass over the data, returning multi-dimensional results in one document. This lesson shows you how to use facets for multi-dimensional aggregation, so you can build rich analytics with performance and elegance.
The problem this lesson solves
Traditional aggregation forces you to run separate queries for every dimension you care about. For an e-commerce store, you might want:
- Total revenue per category
- Top 5 best-selling products
- Monthly order counts for the last year
With separate pipelines, you'd hit the database three times, transfer the same documents over the network three times, and then stitch results together in application code. That's latency, extra load, and messy coordination.
The core problem: you need multiple views of the same data, and you need them fast and consistent. $facet solves this by letting you run several sub-pipelines inside a single aggregation stage, on the same input documents, and get a single output document with one field per sub-pipeline.
Core concept / mental model
Think of $facet as a multi-lens microscope. You place the same specimen under the lens, but each lens shows a different magnification or stains a different structure. You get all the views at once, in one snapshot.
In MongoDB terms:
- Facet = one independent sub-pipeline inside
$facet\n- Input = all documents that reach the$facetstage (after previous stages like$match)\n- Output = a single document where each field name corresponds to a facet, and each value is an array of results from that sub-pipeline
Pipeline-in-a-pipeline
A sub-pipeline inside $facet can contain any aggregation stage except $facet itself (no nesting) and $out / $merge (you can't write from inside a facet). You can use $match, $group, $sort, $limit, $project, $unwind, and more.
Pro tip: Facets are perfect for dashboards — you get all the numbers your UI needs in one network round-trip.
How it works step by step
Let's break down the execution flow of a $facet aggregation.
- Start with an input collection — all documents (or a filtered subset if you have an earlier
$match). - Reach the
$facetstage — the pipeline splits into multiple branches, each operating on the same input documents independently. - Each sub-pipeline runs to completion — filters, groups, sorts, limits, etc., producing an array of result documents.
- Results are gathered — MongoDB collects outputs from every facet and combines them into one output document, with each facet's results stored under a field named after that facet.
- The output document continues — if there are stages after
$facet, they receive that single document.
Why this matters
- One database round trip for many aggregations.
- Consistent snapshot — every facet sees the same input documents (as of that point in the pipeline).
- Parallel execution — MongoDB can run sub-pipelines in parallel internally.
Hands-on walkthrough
Time to write code. We'll use a sample orders collection with documents like:
{
"_id": 1,
"customer": "alice",
"category": "Electronics",
"product": "Laptop",
"price": 1200,
"date": ISODate("2024-01-15")
}
Example 1: Basic $facet with three sub-pipelines
Let's get total revenue per category, top products, and monthly order counts in one go.
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017")
db = client["shop"]
orders = db["orders"]
pipeline = [
{
"$facet": {
"revenueByCategory": [
{"$group": {"_id": "$category", "totalRevenue": {"$sum": "$price"}}},
{"$sort": {"totalRevenue": -1}}
],
"topProducts": [
{"$sort": {"price": -1}},
{"$limit": 3},
{"$project": {"_id": 0, "product": 1, "price": 1}}
],
"ordersPerMonth": [
{
"$group": {
"_id": {"$dateToString": {"format": "%Y-%m", "date": "$date"}},
"count": {"$sum": 1}
}
},
{"$sort": {"_id": 1}}
]
}
}
]
result = list(orders.aggregate(pipeline))
print(result[0])
Expected output (truncated):
{
"revenueByCategory": [
{"_id": "Electronics", "totalRevenue": 1200},
{"_id": "Clothing", "totalRevenue": 150}
],
"topProducts": [
{"product": "Laptop", "price": 1200},
{"product": "Jeans", "price": 150}
],
"ordersPerMonth": [
{"_id": "2024-01", "count": 2},
{"_id": "2024-02", "count": 5}
]
}
Example 2: Filtering before facet
A common pattern is to narrow the data first with $match, then facet on the filtered set.
from pymongo import MongoClient
from datetime import datetime
client = MongoClient("mongodb://localhost:27017")
db = client["shop"]
orders = db["orders"]
start = datetime(2024, 1, 1)
end = datetime(2024, 6, 30)
pipeline = [
{"$match": {"date": {"$gte": start, "$lte": end}}},
{
"$facet": {
"byCustomer": [
{"$group": {"_id": "$customer", "total": {"$sum": "$price"}}},
{"$sort": {"total": -1}},
{"$limit": 5}
],
"totalRevenue": [
{"$group": {"_id": None, "total": {"$sum": "$price"}}}
]
}
}
]
results = list(orders.aggregate(pipeline))
print(results[0])
Expected output:
{
"byCustomer": [
{"_id": "alice", "total": 1300},
{"_id": "bob", "total": 450}
],
"totalRevenue": [
{"_id": null, "total": 1750}
]
}
Example 3: Combining facet with $unwind for tags
If your documents have arrays (like tags), use $unwind inside a facet to analyze elements separately.
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017")
db = client["shop"]
orders = db["orders"]
# Insert sample docs with tags
orders.insert_many([
{"product": "Laptop", "tags": ["tech", "sale"]},
{"product": "Jeans", "tags": ["apparel", "sale"]},
{"product": "Phone", "tags": ["tech", "new"]}
])
pipeline = [
{
"$facet": {
"tagsWithCount": [
{"$unwind": "$tags"},
{"$group": {"_id": "$tags", "count": {"$sum": 1}}},
{"$sort": {"count": -1}}
]
}
}
]
result = list(orders.aggregate(pipeline))
print(result[0])
Expected output:
{
"tagsWithCount": [
{"_id": "sale", "count": 2},
{"_id": "tech", "count": 2},
{"_id": "apparel", "count": 1},
{"_id": "new", "count": 1}
]
}
Compare options / when to choose what
You might wonder when to use $facet versus alternatives. Here's a quick comparison:
| Approach | Pros | Cons | Best for |
|---|---|---|---|
$facet |
One round trip, consistent snapshot, parallel-friendly | Output size can be large if sub-pipelines don't limit | Dashboards, multi-view analytics |
| Multiple separate aggregations | Simple to reason about | Multiple round trips, data may change between queries | When you need truly independent queries |
$group with conditionals |
One pass, but only for simple sums/counts | Limited to same grouping keys | Simple totals (e.g., total revenue and count) |
| Application-side logic | Full flexibility | Inefficient data transfer, code complexity | When MongoDB can't express the logic |
When to choose $facet
- You need multiple summaries of the same filtered dataset.
- You want to reduce network overhead.
- Your data is large and you want parallelism.
When to avoid $facet
- Your sub-pipelines each return massive result sets — you may hit memory limits.
- You need to write results to a different collection (
$outor$merge) — you can't do that inside a facet.
Troubleshooting & edge cases
- Memory limit errors: By default,
$facethas a 100 MB memory limit per facet. If you getExceededMemoryLimit, add a$limitor$projectearly in your sub-pipelines to reduce data. - Output too large: The result document can be huge if facets return many documents. Keep each sub-pipeline limited with
$limit. - Cannot nest $facet: You can't have a
$facetinside a$facetsub-pipeline. MongoDB will throw an error. - Empty sub-pipeline results: If a sub-pipeline matches nothing, it returns an empty array, not
null. Handle that in your application. - Order not guaranteed: The order of documents in each facet's array is not guaranteed unless you include a
$sortinside the sub-pipeline. - Type mismatches: When grouping mixed types, use
$convertor$toStringto avoid surprises.
Pro tip: Always put your biggest filter (
$match) before$facetto reduce the input size and speed up all facets.
What you learned & what's next
You can now use $facet for multi-dimensional aggregation — running multiple sub-pipelines in a single stage, getting consistent results in one round trip, and building efficient dashboards. You saw how to filter before faceting, how to use $unwind inside a facet, and how to avoid common pitfalls like memory limits or nested facets.
Next in the MongoDB track, you'll build on this by learning how to merge or post-process facet results — perhaps using $project and $addFields to shape the output, or combining facets with other advanced stages like $bucket or $sortByCount. That's where your aggregation skills become truly production-ready.
Practice recap
Try building a single $facet pipeline on your own data that returns: (1) total sales per product, (2) count of orders per day, and (3) average order value. Run it with explain() to see how MongoDB optimizes parallel execution, and add a $limit to each facet to stay under memory limits.
Common mistakes
- Forgetting to add
$limitinside sub-pipelines, causing large result arrays and memory issues. - Trying to nest
$facetinside a sub-pipeline — MongoDB throws an error; keep sub-pipelines flat. - Assuming facet output order matches your intent — always include
$sortinside each sub-pipeline. - Leaving
$matchafter the facet, so all documents are processed in every facet, hurting performance.
Variations
- Use
$facetwith$bucketto create histograms for multiple dimensions simultaneously. - Combine
$facetwith$projectand$addFieldsto reshape the output for your API response. - Replace multiple
$groupstages with$facetwhen you need several different grouping keys.
Real-world use cases
- E-commerce dashboard showing revenue by category, top products, and monthly order trends in one API call.
- Log analytics platform displaying error counts by service, endpoint latency percentiles, and active users each hour.
- Marketing campaign reports with impressions by channel, conversion rate, and geographic breakdown all computed together.
Key takeaways
$facetruns multiple sub-pipelines on the same input, producing one document with an array field per facet.- Sub-pipelines cannot contain
$facet,$out, or$merge— keep them independent. - Always filter early with
$matchto reduce input before costly facets. - Add
$sortand$limitinside sub-pipelines for predictable, memory-safe results. - Facet outputs are consistent — all sub-pipelines see the exact same documents at that stage.
- Use
$facetfor dashboards to cut network round trips from several to one.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.