MongoDB $group Stage

Beginner
⏱️ 12 min read
📚 Updated: Jul 2026
🎯 5 Examples
Aggregation

What You’ll Learn

The $group stage is MongoDB’s GROUP BY—it buckets documents by a key (_id) and computes totals, averages, counts, or arrays per bucket. Essential for sales reports, user analytics, and any “summarize by category” query.

01

Group key

The _id field.

02

$sum

Totals.

03

$avg / $max

Stats per bucket.

04

$push

Collect values.

05

_id: null

Grand total.

06

$match first

Filter early.

Definition and Usage

$group is an aggregation stage that groups input documents sharing the same _id value and applies accumulator operators to each group. The output has one document per unique group key—fewer documents than went in (unless every input has a unique _id).

💡
Beginner tip

Think of _id in $group as the column(s) you GROUP BY in SQL—not the document’s MongoDB _id unless you write _id: "$_id". Every other output field must use an accumulator like { total: { $sum: "$amount" } }.

Use $group after $match to filter rows, or after $unwind when grouping exploded array elements. Follow with $sort to rank groups and $project to rename _id for cleaner API responses.

📝 Syntax

$group accepts a document with _id plus accumulator fields:

mongosh
{
  $group: {
    _id: <expression | null>,
    <field1>: { <accumulator>: <expression> },
    <field2>: { <accumulator>: <expression> },
    …
  }
}

Syntax Rules

  • _id — required; defines the group key (field, object, expression, or null).
  • Accumulators only — output fields must use accumulator operators; bare field references are invalid.
  • Common accumulators$sum, $avg, $min, $max, $count, $first, $last, $push, $addToSet.
  • Count trick{ count: { $sum: 1 } } counts documents per group (classic pattern).
  • Memory$group may spill to disk; large $push arrays consume RAM.
  • Order$sort before $group does not affect accumulators except $first/$last.
mongosh
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: "$category",
      totalSales: { $sum: "$amount" },
      orderCount: { $sum: 1 }
    }
  },
  { $sort: { totalSales: -1 } }
])

⚡ Quick Reference

QuestionAnswer
What it doesBuckets documents and runs accumulators per bucket
SQL equivalentGROUP BY with COUNT, SUM, AVG, …
Output sizeOne document per unique _id value
Count per group{ count: { $sum: 1 } } or { count: { $count: {} } }
Grand total_id: null → single summary document
Rename _idFollow with $project: { category: "$_id", … }
Sum revenue
total: {
  $sum: "$amount"
}

Per group

Compound key
_id: {
  country: "$country",
  product: "$product"
}

Multi-field

By year
_id: {
  $year: "$orderDate"
}

Computed key

Unique list
tags: {
  $addToSet: "$tag"
}

No duplicates

🧰 Parameters

The $group document has one required key and any number of accumulator fields:

_id Required

Group key expression. Same value → same bucket. Use null to collapse everything into one group.

_id: "$category"
// or
_id: { region: "$region" }
$sum Accumulator

Adds numeric values. Use $sum: 1 to count documents in each group.

total: { $sum: "$amount" }
count: { $sum: 1 }
$avg / $min / $max Accumulator

Statistical summaries per group—average price, lowest score, highest quantity.

avgPrice: { $avg: "$price" }
maxQty: { $max: "$quantity" }
$push / $addToSet Accumulator

Build arrays per group. $push keeps all values; $addToSet stores unique values only.

orderIds: { $push: "$_id" }
regions: { $addToSet: "$region" }

Examples Gallery

Sample orders collection—group by category, compound keys, grand totals, computed date buckets, and collect order IDs per customer.

📚 Getting Started

E-commerce orders for aggregation labs.

Example 1 — Orders collection

mongosh
db.orders.drop()
db.orders.insertMany([
  { product: "Laptop",  category: "Electronics", country: "US", customer: "Acme Corp", amount: 1200, quantity: 1, status: "completed", orderDate: ISODate("2025-03-10") },
  { product: "Mouse",   category: "Electronics", country: "US", customer: "Acme Corp", amount: 25,   quantity: 2, status: "completed", orderDate: ISODate("2025-03-11") },
  { product: "Desk",    category: "Furniture",   country: "US", customer: "Beta LLC",  amount: 450,  quantity: 1, status: "completed", orderDate: ISODate("2025-04-02") },
  { product: "Chair",   category: "Furniture",   country: "UK", customer: "Beta LLC",  amount: 180,  quantity: 3, status: "completed", orderDate: ISODate("2025-04-15") },
  { product: "Monitor", category: "Electronics", country: "UK", customer: "Gamma Inc", amount: 320,  quantity: 1, status: "completed", orderDate: ISODate("2025-05-01") },
  { product: "Lamp",    category: "Furniture",   country: "DE", customer: "Gamma Inc", amount: 65,   quantity: 2, status: "pending",   orderDate: ISODate("2025-05-20") },
  { product: "Keyboard",category: "Electronics", country: "DE", customer: "Delta Co",  amount: 89,   quantity: 1, status: "completed", orderDate: ISODate("2025-06-08") }
])

db.orders.createIndex({ category: 1, status: 1 })
db.orders.createIndex({ country: 1, product: 1 })

How It Works

Each order has category, country, amount, and status—typical dimensions for $group reports. Index group-by fields when you filter with $match first.

Example 2 — Revenue and count by category

mongosh
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: "$category",
      totalRevenue: { $sum: "$amount" },
      orderCount: { $sum: 1 },
      avgOrderValue: { $avg: "$amount" }
    }
  },
  { $sort: { totalRevenue: -1 } },
  {
    $project: {
      category: "$_id",
      totalRevenue: 1,
      orderCount: 1,
      avgOrderValue: { $round: ["$avgOrderValue", 2] },
      _id: 0
    }
  }
])

/* Example output:
   Electronics → totalRevenue: 1634, orderCount: 4
   Furniture   → totalRevenue: 630,  orderCount: 2
*/

How It Works

All completed orders with the same category land in one bucket. $sum: "$amount" adds revenue; $sum: 1 counts rows— the most common counting idiom in MongoDB aggregation.

📈 Practical Patterns

Multi-field keys, grand totals, and array collectors.

Example 3 — Group by country and product (compound _id)

mongosh
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: { country: "$country", product: "$product" },
      unitsSold: { $sum: "$quantity" },
      revenue: { $sum: "$amount" },
      maxSingleOrder: { $max: "$amount" }
    }
  },
  { $sort: { revenue: -1 } },
  {
    $project: {
      country: "$_id.country",
      product: "$_id.product",
      unitsSold: 1,
      revenue: 1,
      maxSingleOrder: 1,
      _id: 0
    }
  }
])

// Compound _id → separate bucket per country+product pair
// e.g. { country: "US", product: "Laptop" } is its own group

How It Works

When _id is an object, MongoDB compares the whole object for equality—like grouping by multiple columns. Access subfields in later stages with $_id.country.

Example 4 — Grand total with _id: null

mongosh
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: null,
      totalRevenue: { $sum: "$amount" },
      totalUnits: { $sum: "$quantity" },
      orderCount: { $count: {} },
      avgOrderValue: { $avg: "$amount" },
      highestOrder: { $max: "$amount" }
    }
  },
  {
    $project: {
      totalRevenue: 1,
      totalUnits: 1,
      orderCount: 1,
      avgOrderValue: { $round: ["$avgOrderValue", 2] },
      highestOrder: 1,
      _id: 0
    }
  }
])

// _id: null → all matched docs in ONE bucket
// Single summary document for dashboard KPIs

// Group by year (computed _id):
// _id: { year: { $year: "$orderDate" } }

How It Works

_id: null puts every input document into a single group—perfect for dashboard totals. For time-series buckets, use a computed _id like { $year: "$orderDate" } or { $dateToString: { format: "%Y-%m", date: "$orderDate" } }.

Example 5 — Collect order IDs per customer ($push and $addToSet)

mongosh
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: "$customer",
      orderCount: { $sum: 1 },
      totalSpent: { $sum: "$amount" },
      productsOrdered: { $push: "$product" },
      countries: { $addToSet: "$country" }
    }
  },
  { $sort: { totalSpent: -1 } },
  {
    $project: {
      customer: "$_id",
      orderCount: 1,
      totalSpent: 1,
      productsOrdered: 1,
      countries: 1,
      _id: 0
    }
  }
])

// $push → all product names (may repeat)
// $addToSet → unique countries per customer
// See /mongodb/accumulators for $sum, $avg, $topN details

How It Works

$push and $addToSet build arrays inside each group—useful for “list everything this customer bought.” Watch array size on large groups; consider $slice in a later $project if you only need the first N items.

🧠 How $group Works

1

Evaluate _id

MongoDB computes the _id expression for each input document—that value determines which bucket it joins.

Partition
2

Run accumulators

For each bucket, accumulators process every member document—summing amounts, pushing values, tracking min/max.

Aggregate
3

Emit one doc per bucket

Each group becomes a single output document with _id plus accumulator field names.

Output
=

Summary rows

Downstream $sort, $limit, and $project shape the final report or API payload.

📝 Notes

  • _id in $group is the group key, not necessarily the document’s MongoDB _id field.
  • You cannot output a plain field reference—only accumulators (except _id).
  • { count: { $sum: 1 } } and { count: { $count: {} } } both count documents per group.
  • $first / $last depend on input order—add $sort before $group when order matters.
  • Large $push arrays can hit the 100 MB per-stage memory limit—filter or limit before grouping.
  • Previous topic: $graphLookup. Next: $indexStats.

Conclusion

$group is the heart of MongoDB analytics: pick your _id key, attach accumulators for the metrics you need, and sort or project the results for your app.

Filter with $match first, rename _id in $project, and explore dedicated accumulator tutorials for advanced operators. Next: $indexStats for index usage diagnostics.

💡 Best Practices

✅ Do

  • Place $match before $group to reduce documents and use indexes
  • Index fields used in $match and common _id group keys
  • Rename _id with $project for readable API field names
  • Use $addToSet when you need unique values; $push when order and duplicates matter
  • Combine with $sort + $limit for top-N group reports

❌ Don’t

  • Try to pass through raw fields without an accumulator
  • Build huge $push arrays on high-cardinality groups without limits
  • Assume $first is arbitrary—sort first if you need a specific document
  • Use $group when you only need a total count—$count is simpler
  • Forget that null and missing values form separate groups when used as _id

Key Takeaways

Knowledge Unlocked

Five things to remember about $group

Use these points whenever you build summary reports in aggregation.

5
Core concepts
💰 02

$sum

Totals + count.

Metrics
📈 03

$avg / $max

Statistics.

Stats
🗃 04

_id: null

Grand total.

KPIs
🔍 05

$match first

Filter early.

Perf

❓ Frequently Asked Questions

$group partitions input documents into buckets by a group key (_id) and runs accumulator operators on each bucket—like SQL GROUP BY. You get one output document per unique _id value with computed totals, averages, counts, or collected arrays.
_id is the group key—required. It can be a field name ($category), a compound object ({ country: "$country", product: "$product" }), a computed expression ({ year: { $year: "$orderDate" } }), or null for a single grand-total bucket.
No. Every field in a $group output must be either _id or an accumulator result ($sum, $avg, $first, etc.). To keep a field's value, wrap it in $first or $last if all docs in the bucket share the same value.
Common ones: $sum, $avg, $min, $max, $count, $first, $last, $push, $addToSet, $stdDevPop, $stdDevSamp, $mergeObjects, and ranking accumulators ($top, $bottom, $topN, $bottomN on MongoDB 5.2+).
$sortByCount groups by a field and returns count + sort in one shorthand stage. $group is the general form—you choose any _id expression and multiple accumulators, then add $sort yourself.
Yes, when possible. Filtering early reduces documents entering $group, saves memory, and lets indexes on match fields help. Put $match before $group unless you need to aggregate first.
Did you know?

MongoDB 5.0 added $count as a group accumulator ({ n: { $count: {} } }). MongoDB 5.2 introduced ranking accumulators $top, $bottom, $topN, and $bottomN inside $group. The allowDiskUse option lets large grouping stages spill to disk when memory limits are hit. See the official $group docs.

Continue the Stages Series

Summarize with $group, then inspect indexes with $indexStats.

Next: $indexStats →

About the author

Mari Selvan M P
Mari Selvan M P 🔗

Developer, cloud engineer, and technical writer

  • Experience 12 years building web and cloud systems
  • Focus Full Stack Development, AWS, and Developer Education

I write practical tutorials so students and working developers can learn by doing—from databases and APIs to deployment on AWS.

5 people found this page helpful