MongoDB Aggregation Pipeline Tutorial for Beginners in 2025

For the MongoDB beginner who can already insert documents and run find; every stage and operator used here is explained where it appears.

On this page

MongoDB tutorial cover for aggregation pipelines

A find call hands back the documents as they are stored, one by one. The number your report needs is not in any single document: it is an average across a group of them, or a count per month. MongoDB computes that with an aggregation pipeline. You pass db.collection.aggregate() an array of stages, each stage receives the documents the stage before it produced, and the documents leaving the last stage are the answer.

  1. Introduction to MongoDB
  2. Installation and database creation
  3. CRUD operations on documents
  4. Embedded documents and arrays
  5. Validation rules that enforce structure
  6. Visualize MongoDB relationships
  7. What is an index in MongoDB?
  8. Aggregation pipeline explained (you are here)

What an aggregation pipeline is

A pipeline is one or more stages that process documents, as the MongoDB manual defines it. Each stage performs one operation and passes its output to the next stage. A stage does not have to return one document per input document: some stages drop documents, and some build documents that were never stored. The same stage may appear several times in one pipeline, with $out, $merge and $geoNear as the exceptions.

Nothing in the collection changes while a pipeline runs. db.collection.aggregate() leaves the stored documents alone unless the pipeline ends in $merge or $out, so you can try a pipeline on production data and read the answer without writing anything back.

These are the stages this lesson uses, plus the two you will reach for next.

StageWhat it does
$matchkeeps the documents that match a condition
$groupgroups documents and computes values per group
$sortorders the documents
$projectchooses which fields come back
$limitpasses on a fixed number of documents
$addFieldsadds or overwrites a field
$lookuppulls in documents from another collection

Operators do the arithmetic inside a stage. Four of them appear below.

OperatorWhat it computes
$suma total, or a count with $sum: 1
$avgthe average of a numeric field
$monththe month of a date, 1 to 12
$toDatea date from a string or a number

The basic structure of an aggregate call

The call itself is an array of stage documents, in the order MongoDB should apply them.

db.collection.aggregate([
  { stage1 },
  { stage2 }
])

Every stage name begins with a dollar sign, and the position of a stage in that array is part of what the pipeline means. MongoDB runs the array from top to bottom, so a sort placed before a filter sorts documents that the filter is about to throw away, and the same two stages in the other order sort only what survives. The answer is the same either way; the work is not.

Two collections carry the examples: five reviews of one movie, and the movie itself.

db.reviews.insertMany([
  { _id: 1, movie_id: ObjectId("000000000000000000001000"), rating: 5,
    comment: "Absolutely stunning", created_at: ISODate("2024-05-01T00:00:00Z") },
  { _id: 2, movie_id: ObjectId("000000000000000000001000"), rating: 4,
    comment: "Great movie, but a bit hard to follow", created_at: ISODate("2024-05-02T00:00:00Z") },
  { _id: 3, movie_id: ObjectId("000000000000000000001000"), rating: 3,
    comment: "Not bad", created_at: ISODate("2021-01-02T00:00:00Z") },
  { _id: 4, movie_id: ObjectId("000000000000000000001000"), rating: 2,
    comment: "Didn't like it", created_at: ISODate("2023-08-03T00:00:00Z") },
  { _id: 5, movie_id: ObjectId("000000000000000000001000"), rating: 1,
    comment: "The worst movie ever", created_at: ISODate("2019-02-01T00:00:00Z") }
])

db.movie.insertOne({
  _id: ObjectId("000000000000000000001000"),
  title: "Inception",
  genre: "Sci-Fi",
  release_year: 2010,
  details: { duration_min: 148, rating: "PG-13", language: "English" }
})

The created_at values are real dates rather than strings, which matters in the third example: $month accepts a Date, a Timestamp or an ObjectId, and raises an error on a string.

Filtering, sorting and grouping the reviews

Start with the two stages that have a direct equivalent in find. $match narrows the set the way a WHERE clause does, and $sort orders what survives.

db.reviews.aggregate([
  { $match: { movie_id: ObjectId("000000000000000000001000") } },
  { $sort: { rating: -1 } }
])

All five reviews belong to that movie, so all five come back, whole, ordered by rating descending:

_idratingcommentcreated_at
15Absolutely stunning2024-05-01
24Great movie, but a bit hard to follow2024-05-02
33Not bad2021-01-02
42Didn't like it2023-08-03
51The worst movie ever2019-02-01

Put $match first whenever you can. Every later stage then works on a smaller set of documents.

The match and sort pipeline running in the DbSchema Query Editor, with the result grid below it

The screen above shows the same pipeline typed into the DbSchema Query Editor and run against a fuller reviews collection, with the documents it returned printed in a grid under the editor.

Averaging a field with $group and $avg

$group collects documents that share a key and computes a value for each group. The key goes in _id, and every other field of the stage output is an accumulator.

db.reviews.aggregate([
  {
    $group: {
      _id: "$movie_id",
      averageRating: { $avg: "$rating" }
    }
  }
])

The five ratings are 5, 4, 3, 2 and 1, so one group comes back with their mean:

[ { _id: ObjectId("000000000000000000001000"), averageRating: 3 } ]

In the output of $group, _id holds the group key. Write _id: null instead of a field path and every document falls into a single group, which is how you average a whole collection.

The group and avg pipeline in the DbSchema Query Editor, returning one row with an average of 3

Counting per month with $month and $sum

An expression is allowed as the group key, so the month extracted from a date groups the reviews by month. $sum: 1 adds one for every document that lands in the group.

db.reviews.aggregate([
  {
    $group: {
      _id: { $month: "$created_at" },
      totalReviews: { $sum: 1 }
    }
  },
  { $sort: { _id: 1 } }
])

Two reviews were written in May and one each in January, February and August:

[
  { _id: 1, totalReviews: 1 },
  { _id: 2, totalReviews: 1 },
  { _id: 5, totalReviews: 2 },
  { _id: 8, totalReviews: 1 }
]

The $sort at the end is not decoration. $group does not order its output documents, so without it the four groups arrive in whatever order MongoDB produced them. Note also that $month returns 1 to 12 and drops the year: reviews from May 2024 and May 2025 would share the key 5. Group on year and month together when that matters.

How $project picks the fields that come back

$project decides which fields leave the stage. A field set to 1 is included, a field set to 0 is excluded, and _id comes along unless you exclude it explicitly.

db.movie.aggregate([
  {
    $project: {
      title: 1,
      release_year: 1,
      rating: 1
    }
  }
])
[ { _id: ObjectId("000000000000000000001000"), title: "Inception", release_year: 2010 } ]

There is no rating in the answer, and the pipeline asked for one. The movie document keeps its rating inside the details object, and rating: 1 names a top-level field that does not exist, so nothing is added for it. Write the path to reach a nested field.

db.movie.aggregate([
  { $project: { _id: 0, title: 1, "details.rating": 1 } }
])
[ { title: "Inception", details: { rating: "PG-13" } } ]

One restriction is worth knowing before you mix the two forms: once you exclude a field other than _id, that $project stage may not also include fields, rename them, or add new ones. Excluding _id alongside inclusions, as above, is allowed.

A movie collection diagram and a project pipeline in the DbSchema Query Editor

Reports, monthly summaries and per-group averages are all built from these four stages, and the work happens on the server instead of in a loop in your application. The next lesson adds $lookup, the stage that reads documents out of a second collection, which is how a pipeline does what a SQL join does. Download DbSchema at https://dbschema.com/download.html, connect it to your MongoDB database, and paste any pipeline above into the Query Editor to see the documents it returns. Connecting, reverse-engineering the collections and running queries in the Query Editor are covered by the free Community Edition.