Skip to content

Pipeline basics — $match and $project

Two stages carry most aggregation work before anything fancy happens: $match throws away documents you do not need, and $project reshapes the ones you keep. Learn these two well and you already have a query language that goes beyond find. The subtle, important lesson hiding in them is that where you put a stage changes how fast and how correct your pipeline is.

We continue with the sales collection. A single order looks like this:

{
"_id": "a1",
"title": "Dune",
"genre": "fiction",
"quantity": 3,
"price": 12,
"buyer": "Ada"
}

$match filters the stream — put it first

Section titled “$match filters the stream — put it first”

$match keeps only the documents whose fields satisfy its condition. Its syntax is exactly the query syntax you already know from find, so $gt, $in, $and, and the rest all work. The reason to place it first is not style: when $match is the very first stage, the aggregation engine can use a collection’s indexes to skip non-matching documents entirely, instead of streaming the whole collection through later stages.

flowchart LR
  In["all sales"] --> M["$match: price >= 10 (uses index)"]
  M --> P["$project: keep title, compute revenue"]
  P --> Out["slim, reshaped documents"]
Filter first so an index can prune the stream, then reshape what survives

Here we keep only orders of at least ten dollars apiece:

db.sales.aggregate([
{ $match: { price: { $gte: 10 } } }
])

Only the orders priced ten or more survive into the next stage:

[
{ "_id": "a1", "title": "Dune", "genre": "fiction", "quantity": 3, "price": 12, "buyer": "Ada" },
{ "_id": "a4", "title": "Foundation", "genre": "fiction", "quantity": 1, "price": 15, "buyer": "Linus" }
]

$project decides which fields leave the stage and lets you build new fields from expressions. Set a field to 1 to keep it, 0 to drop it, or to an expression to compute it. Field references inside expressions use a $ prefix, so "$price" means “the value of the price field.” Here we keep the title and compute a revenue field as price times quantity:

db.sales.aggregate([
{ $match: { price: { $gte: 10 } } },
{ $project: {
_id: 0,
title: 1,
revenue: { $multiply: ["$price", "$quantity"] }
} }
])

Each surviving document is now slim and carries a computed field:

[
{ "title": "Dune", "revenue": 36 },
{ "title": "Foundation", "revenue": 15 }
]

In Compass: add a $match stage, then an $project stage in the Aggregations tab. The live preview under each stage shows the document count shrinking after $match and the fields changing after $project, which makes the effect of stage order obvious at a glance.

Put $match after a heavy stage and you pay to process documents you were going to discard anyway. Put it first and the engine prunes the stream — sometimes with an index — before any other work happens. The rule of thumb: filter as early as you can, reshape as late as you can. A $project that drops large fields can also help a later stage by shrinking each document, but the biggest win almost always comes from an early $match.

  • An early $match can use indexes; a $match that comes after $group or $project usually cannot, because the documents at that point are freshly computed, not stored.
  • In $project, mixing inclusion (1) and exclusion (0) in the same stage is only allowed for _id. List the fields you want, or drop the ones you do not — not both.
  • Field paths inside expressions need the $ prefix: "$price" is the field’s value, while "price" is the literal string.
  • Prefer $project (or its cousin $set) to compute once and reuse the result downstream, rather than repeating the same expression in several stages.
Why is it best to place $match as the first stage of a pipeline?
In a $project stage, what does "$price" refer to?
Which $project pattern is NOT allowed in a single stage?