Group & Count in MongoDB

Learn to group and count documents with $group and $count in MongoDB. This step-by-step tutorial covers practical examples, beginner-friendly explanations, and advanced tips to master aggregation pipelines.

Focus: group and count documents with $group and $count

Sponsored

You've mastered find(), sort(), limit(), and even basic $match filters, but now the real question hits: how many orders did each customer place? What's the distribution of user signups by month? You need to turn thousands of raw documents into meaningful summaries, and doing this on the client side is slow, memory-hungry, and just plain wrong. This is where MongoDB's aggregation framework steps in—specifically, the $group and $count stages, which let you transform raw data into instant insights right inside the database. In this lesson, you'll learn to group documents by a field and count them, unlocking the power of real-time analytics without ever leaving your MongoDB shell.

The Problem: Summarizing Raw Data Without Aggregation

When you query a MongoDB collection, you get every document that matches your filter—no summaries, no totals. Imagine you have a sales collection with thousands of records. You want to know:

  • How many sales per product?
  • How many customers in each city?
  • How many orders per day?

Trying to do this with find() means pulling every document into your application and counting in a loop. That approach breaks down as data grows, consumes bandwidth, and slows your app. Worse, you're reinventing the wheel for every query.

MongoDB's aggregation pipeline solves this by running the entire transformation on the database server. With the $group and $count stages, you can compute totals, averages, maximums, and more in a single pass. For example, $group can group documents by a field (like product) and return a count for each group, while $count simply returns the total number of documents that reach that stage. These stages are the building blocks for everything from sales dashboards to user analytics.

The pain is real: without aggregation, you're stuck with slow, clunky code. With it, you get fast, scalable, and expressive queries—and you'll wonder how you ever lived without them.

Core Concept / Mental Model: Thinking in Stages

The aggregation pipeline is like an assembly line: documents flow through a series of stages, and each stage transforms the batch before passing it to the next.

  • $group is a grouping station: it takes incoming documents, partitions them by a key (or keys), and then applies accumulator expressions (like $sum, $avg, $max) to produce one output document per group. Think of it like a GROUP BY clause in SQL, but more flexible.

  • $count is a simple counter: it counts how many documents reach that stage and outputs a single document with a specified field name containing the count. It's like a shortcut for $group when you don't need to group by anything—you just want a total.

Key definitions:

  • Country grouping key: the field (or expression) you group by. It's typically the _id value in the $group stage.
  • Accumulator: an expression that summarizes data, e.g., $sum: 1 counts documents, $avg computes average.
  • Pipeline: an array of stages, each transforming the documents.

Analogy: Imagine you have a box of LEGO bricks of different colors. $group sorts them by color, then counts how many of each color are in separate piles. $count just tells you how many bricks are in the entire box before sorting. In MongoDB, $group creates those piles (output documents), and $count gives you one big pile count.

How It Works Step by Step

Let's look at the mechanics, starting with $group.

The $group Stage

The $group stage has this syntax:

{ $group: { _id: <expression>, <field1>: { <accumulator1>: <expression1> }, ... } }

The _id field is where you define what to group by. It can be a field name (like "$product"), a literal value (like null to group everything), or a compound expression (like { year: { $year: "$date" }, month: { $month: "$date" } }).

Then, for each group, you can compute any number of fields using accumulators. To count documents in each group, you use $sum: 1. For example:

db.sales.aggregate([
  { $group: { _id: "$product", count: { $sum: 1 } } }
])

This returns one document per product, with a count field showing how many sales were in that group.

You can also group by multiple fields by passing an expression with a document structure:

{ $group: { _id: { product: "$product", region: "$region" }, count: { $sum: 1 } } }

This groups by both product and region, giving counts per product per region.

The $count Stage

$count is simpler—it just counts all documents that reach it:

db.sales.aggregate([
  { $match: { status: "completed" } },
  { $count: "total_completed" }
])

The output is a single document like { "total_completed": 123 }. This is perfect when you need a quick total after filtering.

Combining Stages

The real power comes from chaining stages. For instance, you might $match to filter, then $group to summarize, then $sort to order results:

db.sales.aggregate([
  { $match: { date: { $gte: ISODate("2024-01-01") } } },
  { $group: { _id: "$product", totalRevenue: { $sum: "$amount" }, count: { $sum: 1 } } },
  { $sort: { totalRevenue: -1 } }
])

This pipeline filters for sales this year, groups by product, computes total revenue and count, and sorts by revenue descending.

Step-by-Step Flow

  1. Filter (optional): Use $match early to reduce the number of documents.
  2. Group: Apply $group to partition and aggregate.
  3. Refine (optional): Use $sort, $limit, or $project to shape the output.
  4. Count (optional): Use $count if you just need a total.

The order matters. $match first drastically reduces the data the pipeline processes, making $group faster.

Hands-On Walkthrough: Let's Get Practical

Let's create a sample collection and run some real queries. Open your mongosh and set up:

// Create a sales collection
db.sales.insertMany([
  { product: "Widget", amount: 10, status: "completed", date: ISODate("2024-01-15") },
  { product: "Gadget", amount: 20, status: "pending", date: ISODate("2024-01-16") },
  { product: "Widget", amount: 15, status: "completed", date: ISODate("2024-02-01") },
  { product: "Gadget", amount: 25, status: "completed", date: ISODate("2024-02-05") },
  { product: "Widget", amount: 30, status: "completed", date: ISODate("2024-03-10") },
])

Example 1: Count Documents with $group

Count how many sales per product:

db.sales.aggregate([
  { $group: { _id: "$product", count: { $sum: 1 } } }
])

Expected Output:

{ "_id": "Widget", "count": 3 }
{ "_id": "Gadget", "count": 2 }

Example 2: Use $count for a Simple Total

Get the total number of completed sales:

db.sales.aggregate([
  { $match: { status: "completed" } },
  { $count: "total_completed" }
])

Expected Output:

{ "total_completed": 4 }

Example 3: Group by Multiple Fields and Sort

Count sales by product and status, then sort by count descending:

db.sales.aggregate([
  { $group: { _id: { product: "$product", status: "$status" }, count: { $sum: 1 } } },
  { $sort: { count: -1 } }
])

Expected Output:

{ "_id": { "product": "Widget", "status": "completed" }, "count": 3 }
{ "_id": { "product": "Gadget", "status": "pending" }, "count": 1 }
{ "_id": { "product": "Gadget", "status": "completed" }, "count": 1 }

Notice how _id becomes a subdocument. This is a common pattern for multi-field grouping.

Example 4: Combine $group and $count in One Pipeline

What if you want to know how many distinct products have sales? You can $group by product, then $count the grouped documents:

db.sales.aggregate([
  { $group: { _id: "$product" } },
  { $count: "distinct_products" }
])

Expected Output:

{ "distinct_products": 2 }

This demonstrates how $group first creates a document per unique product, then $count counts those documents—a powerful two-stage combination.

Compare Options: When to Choose What

When you need to summarize data, you have several choices. Here's a quick comparison:

Approach Use Case Output Example
$group with $sum: 1 Need counts per group or additional aggregates (sum, avg) One doc per group with count and other fields { $group: { _id: "$category", count: { $sum: 1 } } }
$count stage Need just a total count after filtering, no grouping Single doc with a count field { $count: "total" } after $match
countDocuments() Quick total count on a collection, no pipeline needed A number (not a document) db.sales.countDocuments({ status: "completed" })

The key trade-off: countDocuments() is simple but can't do grouping or complex aggregation. $group is the workhorse for grouped summaries—it can compute counts, sums, averages, and more. $count is the lean choice when you just need a total and don't care about grouping details.

Pro tip: If you only need a total count and don't need to transform data, use countDocuments()—it's faster and uses less memory. Reach for $group when you need breakdowns by category, and $count when you're in a pipeline and need a total at a specific stage.

Troubleshooting & Edge Cases

Issue 1: Count is 0 or missing

Symptom: Your $group returns no documents or count shows 0.

Cause: The field you're grouping by might not exist on any document, or your $match filtered everything out.

Fix: Check field names for typos (case-sensitive), and try removing $match temporarily to see raw data.

// Verify field name
db.sales.findOne({ product: { $exists: true } })

Issue 2: _id is unexpected

Symptom: You see _id: null in output.

Cause: You passed a literal value in _id or the grouping expression doesn't reference a field.

Fix: Use _id: "$fieldName" (with the dollar sign) to reference a field. If you see null, it means you used null or a constant that doesn't match any document.

// Correct: references the product field
{ $group: { _id: "$product", ... } }

Issue 3: Memory limit exceeded

Symptom: Error Command failed with error 16945 or the group stage exceeded the 100MB memory limit.

Cause: $group needs to hold all groups in memory. Too many distinct groups or huge data.

Fix: Optimize with $match first, or enable disk usage with allowDiskUse: true:

db.sales.aggregate(
  [ { $group: { _id: "$product", count: { $sum: 1 } } } ],
  { allowDiskUse: true }
)

Issue 4: $count output shape

Symptom: The output isn't what you expected (e.g., field name is different).

Cause: $count uses the name you provide. If you want a different name, change the string.

Fix: { $count: "my_count" } produces { my_count: N }.

What You Learned & What's Next

You've now mastered the core of MongoDB aggregation: grouping documents with $group and counting them with $count. You can:

  • Use $group to partition data and compute counts, sums, averages, and more.
  • Use $count to get a total count at any pipeline stage.
  • Combine stages like $match, $sort, and $limit for powerful analytics.
  • Troubleshoot common issues like memory limits and field name typos.

Your next step in this track is to explore other aggregation accumulators—like $avg, $max, $min, and $push—to build richer summaries. You'll also want to learn about $unwind for working with arrays, and $lookup for joining collections. With $group and $count under your belt, you're well on your way to becoming a MongoDB aggregation pro.

Practice recap

Refresh your memory by experimenting with your sales collection: create groups with $group using different fields, try $count after a $match, and combine both into a pipeline that counts distinct values. You'll quickly build muscle memory for these aggregation essentials.

Common mistakes

  • Using $count expecting a grouped count—it only gives a single total; use $group with $sum: 1 for per-group counts.
  • Forgetting the dollar sign in grouping expressions, e.g., _id: "product" instead of _id: "$product", causing a literal string to be used as the group key.
  • Applying $match after $group when it could be before—pushing filters earlier reduces pipeline memory and speeds up execution.
  • Grouping by a field that doesn't exist, leading to unexpected null groups; always verify the field name and use $exists to check.
  • Assuming $count returns a number directly—it always returns a single document with a named field, so you need to extract it in your application.

Variations

  1. Use $group with a compound _id to group by multiple fields at once, e.g., _id: { product: "$product", region: "$region" }.
  2. Replace $count with $group that has _id: null and count: { $sum: 1 } when you need the count as part of a larger grouped output.
  3. Leverage the $sortByCount stage as a shortcut for grouping by a field and sorting by count in descending order.

Real-world use cases

  • E-commerce dashboard showing sales count per product to identify top sellers and manage inventory.
  • Analytics pipeline that counts user registrations per month to track growth trends and seasonality.
  • Log aggregation system that groups error messages by type and counts occurrences for alerting and debugging.

Key takeaways

  • $group partitions documents by a key and aggregates with accumulators; $count gives a simple total count.
  • Use $sum: 1 inside $group to count documents per group.
  • Combine stages like $match and $sort to filter and order before or after grouping for efficiency.
  • $count outputs a single document with a specified field name, not a raw number.
  • Put $match early in the pipeline to reduce data and avoid memory limits.
  • Handle edge cases like missing fields and memory limits with troubleshooting techniques.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.