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

Sponsored

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 $facet stage (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.

  1. Start with an input collection — all documents (or a filtered subset if you have an earlier $match).
  2. Reach the $facet stage — the pipeline splits into multiple branches, each operating on the same input documents independently.
  3. Each sub-pipeline runs to completion — filters, groups, sorts, limits, etc., producing an array of result documents.
  4. 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.
  5. 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 ($out or $merge) — you can't do that inside a facet.

Troubleshooting & edge cases

  • Memory limit errors: By default, $facet has a 100 MB memory limit per facet. If you get ExceededMemoryLimit, add a $limit or $project early 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 $facet inside a $facet sub-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 $sort inside the sub-pipeline.
  • Type mismatches: When grouping mixed types, use $convert or $toString to avoid surprises.

Pro tip: Always put your biggest filter ($match) before $facet to 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 $limit inside sub-pipelines, causing large result arrays and memory issues.
  • Trying to nest $facet inside a sub-pipeline — MongoDB throws an error; keep sub-pipelines flat.
  • Assuming facet output order matches your intent — always include $sort inside each sub-pipeline.
  • Leaving $match after the facet, so all documents are processed in every facet, hurting performance.

Variations

  1. Use $facet with $bucket to create histograms for multiple dimensions simultaneously.
  2. Combine $facet with $project and $addFields to reshape the output for your API response.
  3. Replace multiple $group stages with $facet when 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

  • $facet runs 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 $match to reduce input before costly facets.
  • Add $sort and $limit inside sub-pipelines for predictable, memory-safe results.
  • Facet outputs are consistent — all sub-pipelines see the exact same documents at that stage.
  • Use $facet for dashboards to cut network round trips from several to one.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.