MongoDB$facet & $bucket Stages

$facet and $bucket / $bucketAuto

$facet runs several independent sub-pipelines over the same input documents in a single pass, returning all their results together — perfect for building a search results page that needs matched items, a total count, and price-range breakdowns all at once. $bucket/$bucketAuto group documents into ranges — the aggregation equivalent of a histogram.

$facet — Multiple Sub-Pipelines in One Pass

Each key inside $facet names an independent sub-pipeline. All of them read from the same set of documents entering the $facet stage — none of them see each other's output, and each produces its own array of results under its key name.

Faceted search: results + total count + category counts, in one query

JS
db.products.aggregate([
  { $match: { category: { $in: ["widgets", "gadgets"] }, inStock: true } },
  {
    $facet: {
      results: [
        { $sort: { price: 1 } },
        { $skip: 0 },
        { $limit: 20 }
      ],
      totalCount: [
        { $count: "count" }
      ],
      categoryCounts: [
        { $group: { _id: "$category", count: { $sum: 1 } } }
      ],
      priceRanges: [
        { $bucket: {
            groupBy: "$price",
            boundaries: [0, 25, 50, 100, 500],
            default: "500+",
            output: { count: { $sum: 1 } }
        } }
      ]
    }
  }
])
[
  {
    results: [ { _id: ..., name: 'Small Widget', price: 4.99, ... }, /* ...19 more */ ],
    totalCount: [ { count: 87 } ],
    categoryCounts: [
      { _id: 'widgets', count: 52 },
      { _id: 'gadgets', count: 35 }
    ],
    priceRanges: [
      { _id: 0,   count: 30 },
      { _id: 25,  count: 40 },
      { _id: 50,  count: 12 },
      { _id: '500+', count: 5 }
    ]
  }
]
Note
$facet always returns a single output document whose fields are the named sub-pipeline results — this is the building block behind almost every e-commerce or search "results + filters sidebar" API response, computed with one round-trip instead of three or four separate queries.
Warning
Each sub-pipeline inside $facet runs against the same input, but $facet stages cannot use indexes for anything after the initial $match — all the sub-pipelines operate on documents already pulled into the $facet stage. Keep the leading $match selective so the amount of data flowing into $facet stays reasonable.
$bucket — Fixed Boundaries

$bucket groups documents into buckets you define explicitly with boundaries — an ascending array of values marking the edges of each range. Every bucket is [boundary[i], boundary[i+1]) — inclusive of the lower edge, exclusive of the upper.

Price histogram with fixed boundaries

JS
db.products.aggregate([
  {
    $bucket: {
      groupBy: "$price",
      boundaries: [0, 25, 50, 100, 500],
      default: "500+",             // catches anything outside all boundaries
      output: {
        count: { $sum: 1 },
        avgPrice: { $avg: "$price" },
        names: { $push: "$name" }
      }
    }
  }
])
Warning
Every document's groupBy value must fall within boundaries or match default — omit default and a document outside every boundary range causes the entire stage to error.
$bucketAuto — Let MongoDB Pick the Boundaries

$bucketAuto doesn't require you to specify boundaries — you just say how many buckets you want, and MongoDB distributes documents into that many groups, trying to keep bucket sizes roughly even.

Automatic 4-bucket price histogram

JS
db.products.aggregate([
  {
    $bucketAuto: {
      groupBy: "$price",
      buckets: 4,
      output: { count: { $sum: 1 }, avgPrice: { $avg: "$price" } }
    }
  }
])
// [
//   { _id: { min: 1.99, max: 15.00 }, count: 62, avgPrice: 8.20 },
//   { _id: { min: 15.00, max: 40.00 }, count: 58, avgPrice: 27.10 },
//   ...
// ]

Stage

Boundaries

Best For

$bucket

You specify exact boundaries

Known, meaningful ranges — price tiers, age brackets, score bands

$bucketAuto

MongoDB picks boundaries for roughly even bucket sizes

Exploratory analysis — quick histograms without deciding ranges up front

Full E-Commerce Faceted Search Example

Putting it all together — a single aggregation that powers a complete search results page: filtered results, pagination info, category filter counts, and a price-range histogram for the sidebar.

Complete faceted search query

JS
const PAGE_SIZE = 20
const page = 1

db.products.aggregate([
  { $match: {
      $text: { $search: "wireless keyboard" },
      inStock: true
  } },
  {
    $facet: {
      results: [
        { $sort: { score: { $meta: "textScore" } } },
        { $skip: (page - 1) * PAGE_SIZE },
        { $limit: PAGE_SIZE },
        { $project: { name: 1, price: 1, category: 1, rating: 1 } }
      ],
      totalCount: [{ $count: "count" }],
      byCategory: [
        { $group: { _id: "$category", count: { $sum: 1 } } },
        { $sort: { count: -1 } }
      ],
      byBrand: [
        { $group: { _id: "$brand", count: { $sum: 1 } } },
        { $sort: { count: -1 } },
        { $limit: 10 }
      ],
      priceHistogram: [
        { $bucketAuto: { groupBy: "$price", buckets: 5 } }
      ]
    }
  },
  {
    $project: {
      results: 1,
      totalCount: { $arrayElemAt: ["$totalCount.count", 0] },
      byCategory: 1,
      byBrand: 1,
      priceHistogram: 1
    }
  }
])
Tip
Because every sub-pipeline in $facet shares the same upstream $match, the results, counts, and filter breakdowns are always consistent with each other — no risk of the result count and the filter sidebar disagreeing because they ran as separate queries at slightly different times.
  • $facet runs multiple named sub-pipelines over the same input, returning all their results together in one document.

  • $bucket groups by explicit boundaries; $bucketAuto lets MongoDB choose roughly even-sized buckets.

  • Keep the $match before $facet selective — sub-pipelines inside $facet can't use indexes.

  • This combination is the standard pattern behind faceted e-commerce / search UIs: results + counts + filters in one round-trip.