$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 runs sub-pipelines in parallel
Section titled “$facet runs sub-pipelines in parallel”$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
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 } } } ] } }])const rows = await db.collection("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 } } }, ], } },]).toArray();rows = list(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}}}, ], }},]))pipeline := mongo.Pipeline{ {{"$facet", bson.D{ {"topOrders", bson.A{ bson.D{{"$sort", bson.D{{"price", -1}}}}, bson.D{{"$limit", 2}}, bson.D{{"$project", bson.D{{"_id", 0}, {"title", 1}, {"price", 1}}}}, }}, {"totalCount", bson.A{ bson.D{{"$count", "orders"}}, }}, {"byGenre", bson.A{ bson.D{{"$group", bson.D{{"_id", "$genre"}, {"count", bson.D{{"$sum", 1}}}}}}, }}, }}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$facet": { "topOrders": [ { "$sort": { "price": -1 } }, { "$limit": 2 }, { "$project": { "_id": 0, "title": 1, "price": 1 } } ], "totalCount": [ { "$count": "orders" } ], "byGenre": [ { "$group": { "_id": "$genre", "count": { "$sum": 1 } } } ] } },];let mut cursor = coll.aggregate(pipeline).await?;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 groups into explicit ranges
Section titled “$bucket groups into explicit ranges”$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" } } } }])const rows = await db.collection("sales").aggregate([ { $bucket: { groupBy: "$price", boundaries: [0, 10, 20, 100], default: "other", output: { count: { $sum: 1 }, titles: { $push: "$title" }, }, } },]).toArray();rows = list(db.sales.aggregate([ {"$bucket": { "groupBy": "$price", "boundaries": [0, 10, 20, 100], "default": "other", "output": { "count": {"$sum": 1}, "titles": {"$push": "$title"}, }, }},]))pipeline := mongo.Pipeline{ {{"$bucket", bson.D{ {"groupBy", "$price"}, {"boundaries", bson.A{0, 10, 20, 100}}, {"default", "other"}, {"output", bson.D{ {"count", bson.D{{"$sum", 1}}}, {"titles", bson.D{{"$push", "$title"}}}, }}, }}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$bucket": { "groupBy": "$price", "boundaries": [0, 10, 20, 100], "default": "other", "output": { "count": { "$sum": 1 }, "titles": { "$push": "$title" } } } },];let mut cursor = coll.aggregate(pipeline).await?;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"] }]$bucketAuto picks the boundaries for you
Section titled “$bucketAuto picks the boundaries for you”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 } }])const rows = await db.collection("sales").aggregate([ { $bucketAuto: { groupBy: "$price", buckets: 3, } },]).toArray();rows = list(db.sales.aggregate([ {"$bucketAuto": { "groupBy": "$price", "buckets": 3, }},]))pipeline := mongo.Pipeline{ {{"$bucketAuto", bson.D{ {"groupBy", "$price"}, {"buckets", 3}, }}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$bucketAuto": { "groupBy": "$price", "buckets": 3 } },];let mut cursor = coll.aggregate(pipeline).await?;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 }]Tips and gotchas
Section titled “Tips and gotchas”- Each
$facetsub-pipeline starts from the same input. Filter before the$facetif you want every facet to share a narrowed set, rather than repeating the$matchinside each one. $facetcannot use an index on its sub-pipelines, since they run over already-streamed documents. Keep the input small by matching early.$bucketrequiresboundariesin ascending order; a value below the first or above the last boundary goes todefault, and without adefaultsuch values raise an error.$bucketAutoaims 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.