Skip to content

$facet and buckets

The last two stages in this module answer questions that need several summaries at once. $facet runs many sub-pipelines side by side over the same input, so you can get a page of results and the total count and a breakdown in a single trip. $bucket and $bucketAuto group documents into ranges, the way a histogram sorts values into bins. Together they power the “dashboard” style query: one request, many panels.

We return to the flat sales collection, where each document is a single order with a price, quantity, and genre.

$facet takes an object whose keys are names you choose and whose values are sub-pipelines. Each sub-pipeline sees the same input documents — the ones that reached the $facet stage — and produces its own array of results. The output is one document with a field per facet. The classic use is “results plus a total count” for a paged listing:

flowchart LR
  In["sales documents"] --> F["$facet"]
  F --> A["pageResults sub-pipeline"]
  F --> B["totalCount sub-pipeline"]
  F --> C["byGenre sub-pipeline"]
  A --> Out["one document with three fields"]
  B --> Out
  C --> Out
One stage, several sub-pipelines over the same input, merged into a single result document
db.sales.aggregate([
{ $facet: {
topOrders: [
{ $sort: { price: -1 } },
{ $limit: 2 },
{ $project: { _id: 0, title: 1, price: 1 } }
],
totalCount: [
{ $count: "orders" }
],
byGenre: [
{ $group: { _id: "$genre", count: { $sum: 1 } } }
]
} }
])

One document comes back, each facet under its own key:

[
{
"topOrders": [
{ "title": "Cosmos", "price": 22 },
{ "title": "Foundation", "price": 15 }
],
"totalCount": [ { "orders": 6 } ],
"byGenre": [
{ "_id": "fiction", "count": 4 },
{ "_id": "science", "count": 2 }
]
}
]

In Compass: $facet is available in the Aggregations tab, and each sub-pipeline can be built and previewed before you combine them. It is the natural way to assemble a results-plus-count query for a paginated screen.

$bucket sorts documents into bins you define by their boundaries. You give a groupBy expression, an ascending array of boundaries, a default bucket for values that fall outside, and an output of accumulators per bucket. Here we bin orders by price into “cheap,” “mid,” and “premium” bands:

db.sales.aggregate([
{ $bucket: {
groupBy: "$price",
boundaries: [0, 10, 20, 100],
default: "other",
output: {
count: { $sum: 1 },
titles: { $push: "$title" }
}
} }
])

Each bucket’s _id is its lower boundary, and the accumulators summarise the orders that landed there:

[
{ "_id": 0, "count": 2, "titles": ["Cheap Reads", "Pocket Atlas"] },
{ "_id": 10, "count": 3, "titles": ["Dune", "Foundation", "Neuromancer"] },
{ "_id": 20, "count": 1, "titles": ["Cosmos"] }
]

When you do not know good boundaries up front, $bucketAuto chooses them so the documents spread across a target number of buckets as evenly as it can. You give a groupBy and a buckets count; it returns each bucket’s min and max inside its _id:

db.sales.aggregate([
{ $bucketAuto: {
groupBy: "$price",
buckets: 3
} }
])

The boundaries are computed for you, each bucket carrying its range and a count:

[
{ "_id": { "min": 8, "max": 12 }, "count": 2 },
{ "_id": { "min": 12, "max": 18 }, "count": 2 },
{ "_id": { "min": 18, "max": 22 }, "count": 2 }
]
  • Each $facet sub-pipeline starts from the same input. Filter before the $facet if you want every facet to share a narrowed set, rather than repeating the $match inside each one.
  • $facet cannot use an index on its sub-pipelines, since they run over already-streamed documents. Keep the input small by matching early.
  • $bucket requires boundaries in ascending order; a value below the first or above the last boundary goes to default, and without a default such values raise an error.
  • $bucketAuto aims for even bucket sizes but will not split identical values across buckets, so the counts can come out uneven when many documents share a value.
What input does each sub-pipeline of a $facet stage receive?
A common use of $facet is to return paged results together with which other thing in one query?
In $bucket, where does a value that falls outside all boundaries go?
How does $bucketAuto differ from $bucket?