MongoDB $group Stage

Beginner
5 min read
Updated: Oct 2026
3 examples
Live preview

What Is $group?

$group 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 and “summarize by category” queries.

Remember
$group = "summarize rows that share a key"

{
  $group: {
    _id: "$category",
    total: { $sum: "$amount" },
    count: { $sum: 1 }
  }
}

_id = group key (required)
every other field = accumulator
$count docs → { $sum: 1 }
grand total → _id: null
compound key → _id: { a: "$a", b: "$b" }

Like SQL GROUP BY + SUM/AVG/COUNT
UsePrefer
Summaries by exact key$group
Count + sort only$sortByCount
Numeric range bands$bucket

How to Use $group in MongoDB

Filter first, pick the group key, then add accumulators.

  1. Optionally filter with $match (fewer docs = cheaper group).
  2. Set _id to the group key (field, object, expression, or null).
  3. Add accumulators: $sum, $avg, $min, $max, $push, …
  4. Optional: $sort / $project the summary rows.
PieceRequired?Role
_idYesGroup key (bucket identity)
Accumulator fieldsNo*Totals / counts / arrays per bucket
$sum: 1—Classic “count documents” idiom
_id: null—One grand-total bucket

*You can group with only _id (unique keys). Reports almost always add accumulators.

Pseudocode

Pseudocode
buckets = empty map

for each document:
  key = evaluate(_id expression)
  put document into buckets[key]
  update accumulators for that bucket
    (sum, avg, min, max, push, …)

for each key in buckets:
  emit { _id: key, …accumulator results }

Cheat Sheet

Keep these facts handy while writing report pipelines.

TopicDetail
TypeAggregation stage
OutputOne document per unique _id
Count docs{ count: { $sum: 1 } }
Sum field{ total: { $sum: "$amount" } }
Keep a fieldUse $first / $last (not bare field)
Compound key_id: { a: "$a", b: "$b" }
Grand total_id: null

Live Preview

Group the sample completed orders by category or country (teaching model).

Uses completed orders only (pending Lamp excluded).

Result
{ _id: "Electronics", totalRevenue: 1634, orderCount: 4 }
{ _id: "Furniture", totalRevenue: 630, orderCount: 2 }

Worked Walkthrough — By Category

Completed orders only. Goal: total revenue and order count per category.

Calculation
Completed amounts:
  Electronics: 1200 + 25 + 320 + 89 = 1634  (4 orders)
  Furniture:   450 + 180           = 630   (2 orders)
  Lamp pending → excluded by $match

$output:
  { _id: "Electronics", totalRevenue: 1634, orderCount: 4 }
  { _id: "Furniture",   totalRevenue: 630,  orderCount: 2 }

MongoDB Examples

We use a small orders collection. First summarize by category, then compute a grand total. Run these in mongosh after inserting the sample data once.

Example 1 — Sample Data

Seven orders across categories and countries—enough for category totals and a grand total.

mongosh
db.orders.drop()
db.orders.insertMany([
  { product: "Laptop",   category: "Electronics", country: "US", amount: 1200, status: "completed" },
  { product: "Mouse",    category: "Electronics", country: "US", amount: 25,   status: "completed" },
  { product: "Desk",     category: "Furniture",   country: "US", amount: 450,  status: "completed" },
  { product: "Chair",    category: "Furniture",   country: "UK", amount: 180,  status: "completed" },
  { product: "Monitor",  category: "Electronics", country: "UK", amount: 320,  status: "completed" },
  { product: "Lamp",     category: "Furniture",   country: "DE", amount: 65,   status: "pending" },
  { product: "Keyboard", category: "Electronics", country: "DE", amount: 89,   status: "completed" }
])

How the Data Is Stored in MongoDB

After the insert, MongoDB keeps seven documents in orders. Run find() to see them:

mongosh
db.orders.find()

Read the important fields like this:

FieldExamplePlain English
category"Electronics"Common group key
amount1200Value we sum / average
status"completed"Filter before grouping

How It Works

  1. Reset once. drop() keeps demos repeatable.
  2. Mixed dimensions. Category and country are natural group keys.
  3. One pending row. Later examples filter it out with $match.

Example 2 — By Category

Goal: for completed orders, compute revenue, order count, and average order value per 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
    }
  }
])

How It Works

  1. Filter first. $match drops the pending Lamp order.
  2. Bucket by category. Same category → same group.
  3. Accumulate. $sum / $avg / $sum: 1 fill the summary fields.
  4. Present. $sort and $project make a clean report shape.

Example 3 — Grand Total

Goal: one summary for all completed orders using _id: null. Also show a compound key pattern.

mongosh
// One bucket for everything
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: null,
      totalRevenue: { $sum: "$amount" },
      orderCount: { $sum: 1 },
      products: { $addToSet: "$product" }
    }
  },
  {
    $project: {
      _id: 0,
      totalRevenue: 1,
      orderCount: 1,
      products: 1
    }
  }
])

// Compound key (country + category) — each pair is its own bucket:
// _id: { country: "$country", category: "$category" }

How It Works

  1. _id: null. Every input row lands in one bucket.
  2. Same accumulators. Sum and count work like category grouping.
  3. $addToSet. Collects unique product names (no duplicates).
  4. Compound tip. An object _id groups by multiple fields at once.

Edge Cases & Pitfalls

Check these before shipping report pipelines with $group.

fields

No bare fields beside _id

Every non-_id field needs an accumulator. Use $first / $last to carry a value through.

mem

Large $push arrays cost RAM

Collecting huge arrays per group can get expensive. Prefer counts/sums when the UI does not need every raw value.

sort

$sort before $group is limited

Order into $group mainly matters for $first / $last. Sort summary rows after grouping for report order.

null

Missing group keys become null

Documents without the group field share an _id: null bucket (separate from intentional grand-total _id: null when that is your only key).

Performance Notes

ApproachCostBest for
$match then $groupFewer docs to bucketMost reports
$group + light accumulatorsLower memoryDashboards / APIs
Huge $push per groupHigh memoryAvoid unless required

Filter early with $match. Prefer $sum / $avg over collecting every document when a number answers the question.

Key Takeaways

  • Role: GROUP BY with accumulators per bucket.
  • _id: the group key; everything else accumulates.
  • Count: { $sum: 1 } per group.
  • Grand total: _id: null.

One line: $group answers “summarize these documents by this key.”

Frequently Asked Questions

$group partitions input documents into buckets by a group key (_id) and runs accumulators on each bucket—like SQL GROUP BY. You get one output document per unique _id with totals, averages, counts, or collected arrays.
_id is the required group key. It can be a field ("$category"), a compound object ({ country: "$country", product: "$product" }), a computed expression, or null for a single grand-total bucket.
No. Every field besides _id must be an accumulator result ($sum, $avg, $first, etc.). To keep a field’s value, wrap it in $first or $last when all docs in the bucket share it.
Common ones: $sum, $avg, $min, $max, $count, $first, $last, $push, and $addToSet. Counting documents is often { count: { $sum: 1 } }.
$sortByCount groups by a field and returns count + sort in one shorthand. $group is the general form—any _id expression and multiple accumulators—then you add $sort yourself.
Yes when possible. Filtering early reduces documents entering $group, saves memory, and lets indexes help. Put $match before $group unless you must aggregate first.

Next: $indexStats

Inspect index usage statistics to find unused or hot indexes.

$indexStats tutorial →

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.

9 people found this page helpful