$group takes an _id saying what makes two documents the same, and accumulators that fold every document in a group into one value. The output carries the group key as _id plus what the accumulators produced. { $sum: 1 } counts, because it adds the constant 1 once per document.
db.orders.aggregate([
{ $unwind: '$items' },
{ $group: { _id: '$items.category', units: { $sum: '$items.qty' },
best: { $top: { output: '$items.sku', sortBy: { 'items.price': -1 } } },
avg: { $avg: '$items.price' } } },
{ $sort: { _id: 1 } }
])[
{ _id: 'accessories', units: 16, best: 'AC-51', avg: 26.25 },
{ _id: 'audio', units: 9, best: 'AU-31', avg: 120 },
{ _id: 'displays', units: 6, best: 'DS-42', avg: 448 },
{ _id: 'keyboards', units: 6, best: 'KB-11', avg: 108 }
]The _id can be any expression. null folds the whole stream into one document; a subdocument makes a compound key that stays nested, so later stages address it as '_id.status'; and a computed value such as { $dateToString: { format: '%Y-%m', date: '$placedAt' } } buckets by month.
| Accumulator | Notes |
|---|---|
| $sum, $avg, $min, $max | Ignore missing values; BSON type order applies |
| $count | Shorthand for { $sum: 1 } (5.0+) |
| $push, $addToSet | Build arrays; $addToSet is unordered |
| $first, $last | Meaningful only after an explicit $sort |
| $top, $topN, $bottom, $bottomN | Carry their own sortBy (5.2+) |
| $stdDevPop, $stdDevSamp, $median | Also $percentile; t-digest based (7.0+) |
| $mergeObjects | Merges subdocuments, later keys winning |
$top is the one to learn first, because the obvious alternative is worse: sorting the whole stream by price and taking $first forces a blocking sort, while $top keeps a single candidate per group.