Skip to content

$group and accumulators

$group is where aggregation earns its name. It collapses many documents into one document per distinct value of a key you choose, and for each group it computes summaries with accumulators. This is the stage that answers “how many per genre,” “average price per buyer,” “biggest order per title.” If you have written GROUP BY in SQL, the instinct is the same; the spelling is different.

We stay with the sales collection from the previous lessons.

The _id of a $group stage is the grouping key — the field (or computed value) that documents are bucketed by. Every other field in the stage is an accumulator: an expression that folds all the documents in a group into a single value. Here we total the quantity sold per genre and also count the orders:

flowchart LR
  In["sales documents"] --> G["$group _id genre"]
  G --> A["accumulators: $sum quantity, $sum 1"]
  A --> Out["one document per genre"]
Many documents fold into one result document for each distinct group key
db.sales.aggregate([
{ $group: {
_id: "$genre",
totalCopies: { $sum: "$quantity" },
orders: { $sum: 1 }
} }
])

One document comes back per genre, each carrying its summaries:

[
{ "_id": "fiction", "totalCopies": 11, "orders": 4 },
{ "_id": "science", "totalCopies": 5, "orders": 2 }
]

A handful of accumulators cover most needs. They all take an expression evaluated against each document in the group:

  • $sum — adds up a numeric expression, or counts with $sum: 1.
  • $avg — the mean of a numeric expression.
  • $min / $max — the smallest and largest value seen.
  • $push — collects every value into an array, duplicates included.
  • $addToSet — collects distinct values into an array.

A dedicated $count stage also exists for the special case of “just count the documents.” Here we profile each genre more fully — average price, the price range, and the set of buyers:

db.sales.aggregate([
{ $group: {
_id: "$genre",
avgPrice: { $avg: "$price" },
cheapest: { $min: "$price" },
priciest: { $max: "$price" },
buyers: { $addToSet: "$buyer" }
} }
])

Each genre now has a full profile, and buyers holds each name only once:

[
{ "_id": "fiction", "avgPrice": 12.5, "cheapest": 9, "priciest": 15, "buyers": ["Ada", "Linus"] },
{ "_id": "science", "avgPrice": 20, "cheapest": 18, "priciest": 22, "buyers": ["Grace"] }
]

In Compass: the $group stage in the Aggregations tab gives you the same _id and accumulator structure. The preview is handy here — change the _id expression and watch the number of result documents change as the grouping coarsens or splits.

Set the group _id to null and every document falls into a single group, giving you a grand total across the whole stream. This is how you compute “total revenue” or “overall average” with no per-key split:

db.sales.aggregate([
{ $group: {
_id: null,
totalRevenue: { $sum: { $multiply: ["$price", "$quantity"] } },
totalOrders: { $sum: 1 }
} }
])

A single summary document represents the whole collection:

[
{ "_id": null, "totalRevenue": 248, "totalOrders": 6 }
]
  • After $group, only the fields you produced exist. The original document fields are gone unless you carried them through with an accumulator like $push or $first.
  • $sum: 1 is the idiomatic “count rows in this group.” $sum: "$quantity" adds a field’s values instead.
  • $push keeps duplicates and order; $addToSet removes duplicates and does not promise an order. Reach for $addToSet when you want a distinct list.
  • Grouping does not sort. If you want the largest group first, add a $sort stage after $group.
What does the _id field of a $group stage specify?
How do you count the number of documents in each group?
Which accumulator collects only the distinct values into an array?
What is the effect of setting a $group _id to null?