Aggregation Basics

Analyse data inside MongoDB with the aggregation pipeline: filter with $match, group and total with $group and accumulators like $sum and $avg, sort and limit results, and reshape documents with $project.

Lesson 12 of 43Intermediate19 min read

Finding documents is only half of what applications need. Dashboards and reports ask questions about groups of documents: total sales per month, average rating per product, the five best-selling items, the number of orders in each status. Pulling every document into your application to add them up is slow and wasteful. MongoDB's aggregation pipeline does the work inside the database.

A pipeline is an array of stages. Documents flow through the stages in order, and each stage transforms them: $match filters, $group combines documents into groups and calculates totals, $sort orders them, $limit keeps the first few and $project reshapes each one. The output of one stage is the input of the next, like an assembly line.

Aggregation is MongoDB's equivalent of SQL's GROUP BY, SUM, AVG and much more. In this lesson you'll build pipelines step by step with the most important stages and accumulators. The next lesson adds stages for arrays, buckets and multiple results. The examples use this sales collection:

MongoDB Shell
db.sales.insertMany([
  { _id: 1, item: 'Mug', category: 'kitchen', qty: 2, price: 8.5, month: 1, region: 'North' },
  { _id: 2, item: 'Lamp', category: 'home', qty: 1, price: 24, month: 1, region: 'South' },
  { _id: 3, item: 'Mug', category: 'kitchen', qty: 5, price: 8.5, month: 2, region: 'North' },
  { _id: 4, item: 'Kettle', category: 'kitchen', qty: 1, price: 30, month: 2, region: 'South' },
  { _id: 5, item: 'Rug', category: 'home', qty: 1, price: 80, month: 2, region: 'North' },
  { _id: 6, item: 'Lamp', category: 'home', qty: 3, price: 24, month: 3, region: 'North' }
])

Pipelines and stages

MongoDB Shell
db.sales.aggregate([
  { $match: { region: 'North' } },                       // 1. keep North sales
  { $group: { _id: '$category', units: { $sum: '$qty' } } },  // 2. total units per category
  { $sort: { units: -1 } }                               // 3. biggest first
])
  • aggregate takes an array of stages and returns a cursor of results.
  • '$fieldName', a string starting with $, refers to a field's value in the current document.
  • Put $match first whenever you can: it reduces the documents later stages handle, and at the start of a pipeline it can use indexes.
  • Stages can repeat and appear in any order that makes sense.

$group and accumulators

$group collects documents with the same _id value into one result document:

AccumulatorResult per group
$sum: '$qty'Total of a field
$sum: 1Number of documents
$avg: '$price'Average
$min, $maxSmallest and largest value
$first, $lastValue from the first or last document (after sorting)
$push: '$item'Array of every value
$addToSet: '$item'Array of unique values

Group by several fields with an object: _id: { month: '$month', region: '$region' }. Use _id: null to calculate totals across all documents. Accumulators can work on expressions too: { $sum: { $multiply: ['$qty', '$price'] } } totals revenue.

Reshaping with $project and $set

  • $project chooses and computes the fields of each output document: { $project: { _id: 0, category: '$_id', revenue: { $round: ['$revenue', 2] } } }.
  • $set (also called $addFields) adds or replaces fields and keeps all the others.
  • $sort, $limit and $skip work like their find equivalents, and $count outputs the number of documents reaching it.
  • A $match after $group filters groups, like SQL's HAVING.

What $group doesn't promise, and memory limits

$group returns its groups in no particular order. On a small collection you might see the same order every time, but it isn't guaranteed and can change between runs or versions. If order matters, for a report or a chart, always add a $sort after $group, for example { $sort: { _id: 1 } } to sort by the grouping key.

The same goes for $first and $last: they mean "first or last document the group received", so they're only meaningful when a $sort stage comes before the $group. Without one, the order documents arrive in depends on how they happen to be stored.

Stages that have to hold many documents at once, such as $group and $sort, can use up to 100 MB of memory each. If they need more, MongoDB writes temporary files to disk. Since MongoDB 6.0 that happens automatically, because the server setting allowDiskUseByDefault is true. You can still control it per query:

MongoDB Shell
db.sales.aggregate([...], { allowDiskUse: false })   // fail instead of spilling to disk

Spilling to disk keeps big pipelines working but makes them slower. When a pipeline regularly needs it, look for ways to shrink its input first: a stricter $match, a $project that drops large fields early, or a summary collection kept up to date with $merge.

MongoDB Aggregation Syntax

Aggregation essentials

db.c.aggregate([
  { $match: { status: 'paid' } },
  { $group: { _id: '$customerId', orders: { $sum: 1 }, spent: { $sum: '$total' } } },
  { $match: { spent: { $gt: 100 } } },
  { $sort: { spent: -1 } },
  { $limit: 5 },
  { $project: { _id: 0, customerId: '$_id', orders: 1, spent: { $round: ['$spent', 2] } } }
])
{ $group: { _id: null, total: { $sum: '$amount' } } }
{ $group: { _id: { y: { $year: '$at' }, m: { $month: '$at' } }, n: { $sum: 1 } } }

MongoDB Aggregation Summary

  • aggregate([...]) runs documents through a pipeline of stages in order.
  • $match filters (put it first to use indexes); $group combines documents by _id.
  • Accumulators such as $sum, $avg, $min, $max, $push and $addToSet calculate values per group.
  • '$field' refers to a field's value; _id: null groups everything together.
  • $project and $set reshape documents; $sort and $limit give top-N results.

MongoDB Aggregation Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Revenue per category

MongoDB Shell
db.sales.aggregate([
  { $group: {
      _id: '$category',
      orders: { $sum: 1 },
      units: { $sum: '$qty' },
      revenue: { $sum: { $multiply: ['$qty', '$price'] } }
  } },
  { $sort: { revenue: -1 } }
])
Output
[
  { _id: 'home', orders: 3, units: 5, revenue: 176 },
  { _id: 'kitchen', orders: 3, units: 8, revenue: 89.5 }
]
Line-by-line explanation
  1. Line 3Documents with the same category end up in one group.
  2. Line 4$sum: 1 counts the documents in each group.
  3. Line 5$sum of a field adds up its values.
  4. Line 6Accumulators can use expressions, here quantity times price.
  5. Line 8Sort the groups by revenue, highest first.
Example 2

Filtering, top items and totals

MongoDB Shell
db.sales.aggregate([
  { $match: { region: 'North' } },
  { $group: { _id: '$item', units: { $sum: '$qty' } } },
  { $sort: { units: -1, _id: 1 } },
  { $limit: 2 }
])

db.sales.aggregate([
  { $group: { _id: null, totalUnits: { $sum: '$qty' }, avgPrice: { $avg: '$price' }, maxPrice: { $max: '$price' } } },
  { $project: { _id: 0, totalUnits: 1, avgPrice: { $round: ['$avgPrice', 2] }, maxPrice: 1 } }
])
Output
[ { _id: 'Mug', units: 7 }, { _id: 'Lamp', units: 3 } ]
[ { totalUnits: 13, maxPrice: 80, avgPrice: 29.17 } ]
Line-by-line explanation
  1. Line 2$match first keeps only North sales, reducing work for later stages.
  2. Lines 4 to 5Sorting then limiting gives a top-N list; _id breaks ties.
  3. Line 9_id: null puts every document in one group for overall totals.
  4. Line 10$project hides _id and rounds the average to two decimal places.
Example 3

Grouping by several fields with $push

MongoDB Shell
db.sales.aggregate([
  { $group: {
      _id: { month: '$month', region: '$region' },
      items: { $push: '$item' },
      distinctItems: { $addToSet: '$item' }
  } },
  { $match: { 'items.1': { $exists: true } } },
  { $project: { _id: 0, month: '$_id.month', region: '$_id.region', items: 1 } },
  { $sort: { month: 1 } }
])
Output
[ { items: [ 'Mug', 'Rug' ], month: 2, region: 'North' } ]
Line-by-line explanation
  1. Line 3An object as _id groups by month and region together.
  2. Line 4$push collects every item in the group into an array.
  3. Line 5$addToSet collects unique items only.
  4. Line 7A $match after $group filters groups, like SQL's HAVING: here, groups with at least two sales.
  5. Line 8$project flattens the compound _id into separate fields.

Common MongoDB Aggregation Mistakes

Forgetting the $ on field paths

Don't
{ $group: { _id: 'category', total: { $sum: 'qty' } } }   // groups by the literal text "category"; total is 0
Do
{ $group: { _id: '$category', total: { $sum: '$qty' } } }

Filtering after grouping when you could filter first

Don't
[{ $group: { _id: '$region', n: { $sum: 1 } } }, { $match: { _id: 'North' } }]   // groups every document first
Do
[{ $match: { region: 'North' } }, { $group: { _id: '$region', n: { $sum: 1 } } }]

Totalling in application code

Don't
let total = 0
db.sales.find().forEach(s => { total += s.qty * s.price })   // moves every document over the network
Do
db.sales.aggregate([{ $group: { _id: null, total: { $sum: { $multiply: ['$qty', '$price'] } } } }])

MongoDB Aggregation in Real Projects

Aggregation in practice

Admin dashboards show revenue per day, top products and orders per status with aggregation pipelines. Analytics features compute averages, counts and leaderboards, and scheduled jobs build summary collections with $merge so dashboards load instantly. Atlas Charts and many BI tools read MongoDB through pipelines, and the same pipelines run from Node.js with collection.aggregate([...]).toArray().

MongoDB Aggregation FAQ

What is the difference between find and aggregate in MongoDB?

find filters, projects, sorts and pages documents, returning them more or less as they're stored. aggregate runs a pipeline of stages that can also group, compute new fields, unwind arrays, join collections and write results to another collection. If find can answer your question, use it; reach for aggregate when you need totals, reshaping or joins.

How do I count documents per group in MongoDB?

Group by the field and add $sum: 1 for each document: db.orders.aggregate([{ $group: { _id: '$status', count: { $sum: 1 } } }]). The $sortByCount: '$status' stage does the same and also sorts by count, largest first. Add { $sort: { _id: 1 } } if you'd rather list the groups alphabetically.

How do I group by date in a MongoDB aggregation?

Turn each date into the period you want and group on that. { $group: { _id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }, total: { $sum: '$amount' } } } totals per day. For weeks or months, $dateTrunc with unit: 'week' or 'month' keeps a real Date as the key, and both operators accept a timezone so days follow local time.

How do I filter results after grouping, like HAVING in SQL?

Add a $match stage after the $group and filter on the fields it produced: { $match: { count: { $gt: 10 } } }. A $match before $group filters input documents, like WHERE, while one after filters the groups, like HAVING. Use both when you need them.

Can an aggregation pipeline use indexes?

Yes, but mainly at the start. A $match and $sort at the beginning of a pipeline can use indexes just like a find. Once documents have been reshaped by stages such as $group, $project or $unwind, later stages work on the new documents in memory, so put your filtering first.

MongoDB Aggregation Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the stage that keeps only paid orders.

db.orders.aggregate([{ : { status: 'paid' } }])
Multiple choiceeasy

What does $group's _id decide?

Predict the outputmedium

Using the sales collection, how many groups does this produce?

db.sales.aggregate([{ $group: { _id: '$month' } }])
Multiple choicemedium

How do you count the documents in each group?

Multiple choicehard

Why should $match usually come first?

MongoDB Aggregation Quiz

5 questions to check what you learned.

  1. What is an aggregation pipeline?

    • A backup process
    • A type of index
    • A replication method
    • An array of stages that documents pass through in order
  2. What does _id: null in $group do?

    • Removes _id from results
    • Causes an error
    • Groups all documents into a single group
    • Groups by missing values only
  3. Which accumulator builds an array of unique values?

    • $push
    • $unique
    • $distinct
    • $addToSet
  4. What does a $match placed after $group act like in SQL?

    • HAVING
    • WHERE
    • ORDER BY
    • JOIN
  5. What does '$price' mean inside a pipeline stage?

    • The text "price"
    • The value of the price field in the current document
    • An operator called price
    • The average price