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
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.
-
$groupis 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 aGROUP BYclause in SQL, but more flexible. -
$countis 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$groupwhen 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
_idvalue in the$groupstage. - Accumulator: an expression that summarizes data, e.g.,
$sum: 1counts documents,$avgcomputes 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
- Filter (optional): Use
$matchearly to reduce the number of documents. - Group: Apply
$groupto partition and aggregate. - Refine (optional): Use
$sort,$limit, or$projectto shape the output. - Count (optional): Use
$countif 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$groupwhen you need breakdowns by category, and$countwhen 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
$groupto partition data and compute counts, sums, averages, and more. - Use
$countto get a total count at any pipeline stage. - Combine stages like
$match,$sort, and$limitfor 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
$countexpecting a grouped count—it only gives a single total; use$groupwith$sum: 1for 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
$matchafter$groupwhen 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
nullgroups; always verify the field name and use$existsto check. - Assuming
$countreturns a number directly—it always returns a single document with a named field, so you need to extract it in your application.
Variations
- Use
$groupwith a compound_idto group by multiple fields at once, e.g.,_id: { product: "$product", region: "$region" }. - Replace
$countwith$groupthat has_id: nullandcount: { $sum: 1 }when you need the count as part of a larger grouped output. - Leverage the
$sortByCountstage 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
$grouppartitions documents by a key and aggregates with accumulators;$countgives a simple total count.- Use
$sum: 1inside$groupto count documents per group. - Combine stages like
$matchand$sortto filter and order before or after grouping for efficiency. $countoutputs a single document with a specified field name, not a raw number.- Put
$matchearly in the pipeline to reduce data and avoid memory limits. - Handle edge cases like missing fields and memory limits with troubleshooting techniques.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.