$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.
Grouping by a key
Section titled “Grouping by a key”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"]
db.sales.aggregate([ { $group: { _id: "$genre", totalCopies: { $sum: "$quantity" }, orders: { $sum: 1 } } }])const rows = await db.collection("sales").aggregate([ { $group: { _id: "$genre", totalCopies: { $sum: "$quantity" }, orders: { $sum: 1 }, } },]).toArray();rows = list(db.sales.aggregate([ {"$group": { "_id": "$genre", "totalCopies": {"$sum": "$quantity"}, "orders": {"$sum": 1}, }},]))pipeline := mongo.Pipeline{ {{"$group", bson.D{ {"_id", "$genre"}, {"totalCopies", bson.D{{"$sum", "$quantity"}}}, {"orders", bson.D{{"$sum", 1}}}, }}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$group": { "_id": "$genre", "totalCopies": { "$sum": "$quantity" }, "orders": { "$sum": 1 } } },];let mut cursor = coll.aggregate(pipeline).await?;One document comes back per genre, each carrying its summaries:
[ { "_id": "fiction", "totalCopies": 11, "orders": 4 }, { "_id": "science", "totalCopies": 5, "orders": 2 }]The common accumulators
Section titled “The common accumulators”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" } } }])const rows = await db.collection("sales").aggregate([ { $group: { _id: "$genre", avgPrice: { $avg: "$price" }, cheapest: { $min: "$price" }, priciest: { $max: "$price" }, buyers: { $addToSet: "$buyer" }, } },]).toArray();rows = list(db.sales.aggregate([ {"$group": { "_id": "$genre", "avgPrice": {"$avg": "$price"}, "cheapest": {"$min": "$price"}, "priciest": {"$max": "$price"}, "buyers": {"$addToSet": "$buyer"}, }},]))pipeline := mongo.Pipeline{ {{"$group", bson.D{ {"_id", "$genre"}, {"avgPrice", bson.D{{"$avg", "$price"}}}, {"cheapest", bson.D{{"$min", "$price"}}}, {"priciest", bson.D{{"$max", "$price"}}}, {"buyers", bson.D{{"$addToSet", "$buyer"}}}, }}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$group": { "_id": "$genre", "avgPrice": { "$avg": "$price" }, "cheapest": { "$min": "$price" }, "priciest": { "$max": "$price" }, "buyers": { "$addToSet": "$buyer" } } },];let mut cursor = coll.aggregate(pipeline).await?;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.
Grand totals with _id null
Section titled “Grand totals with _id null”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 } } }])const rows = await db.collection("sales").aggregate([ { $group: { _id: null, totalRevenue: { $sum: { $multiply: ["$price", "$quantity"] } }, totalOrders: { $sum: 1 }, } },]).toArray();rows = list(db.sales.aggregate([ {"$group": { "_id": None, "totalRevenue": {"$sum": {"$multiply": ["$price", "$quantity"]}}, "totalOrders": {"$sum": 1}, }},]))pipeline := mongo.Pipeline{ {{"$group", bson.D{ {"_id", nil}, {"totalRevenue", bson.D{{"$sum", bson.D{{"$multiply", bson.A{"$price", "$quantity"}}}}}}, {"totalOrders", bson.D{{"$sum", 1}}}, }}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$group": { "_id": null, "totalRevenue": { "$sum": { "$multiply": ["$price", "$quantity"] } }, "totalOrders": { "$sum": 1 } } },];let mut cursor = coll.aggregate(pipeline).await?;A single summary document represents the whole collection:
[ { "_id": null, "totalRevenue": 248, "totalOrders": 6 }]Tips and gotchas
Section titled “Tips and gotchas”- After
$group, only the fields you produced exist. The original document fields are gone unless you carried them through with an accumulator like$pushor$first. $sum: 1is the idiomatic “count rows in this group.”$sum: "$quantity"adds a field’s values instead.$pushkeeps duplicates and order;$addToSetremoves duplicates and does not promise an order. Reach for$addToSetwhen you want a distinct list.- Grouping does not sort. If you want the largest group first, add a
$sortstage after$group.