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"]
Here we keep only orders of at least ten dollars apiece:
db.sales.aggregate([ { $match: { price: { $gte: 10 } } }])const rows = await db.collection("sales").aggregate([ { $match: { price: { $gte: 10 } } },]).toArray();rows = list(db.sales.aggregate([ {"$match": {"price": {"$gte": 10}}},]))pipeline := mongo.Pipeline{ {{"$match", bson.D{{"price", bson.D{{"$gte", 10}}}}}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$match": { "price": { "$gte": 10 } } },];let mut cursor = coll.aggregate(pipeline).await?;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 reshapes and computes
Section titled “$project reshapes and computes”$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"] } } }])const rows = await db.collection("sales").aggregate([ { $match: { price: { $gte: 10 } } }, { $project: { _id: 0, title: 1, revenue: { $multiply: ["$price", "$quantity"] }, } },]).toArray();rows = list(db.sales.aggregate([ {"$match": {"price": {"$gte": 10}}}, {"$project": { "_id": 0, "title": 1, "revenue": {"$multiply": ["$price", "$quantity"]}, }},]))pipeline := mongo.Pipeline{ {{"$match", bson.D{{"price", bson.D{{"$gte", 10}}}}}}, {{"$project", bson.D{ {"_id", 0}, {"title", 1}, {"revenue", bson.D{{"$multiply", bson.A{"$price", "$quantity"}}}}, }}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$match": { "price": { "$gte": 10 } } }, doc! { "$project": { "_id": 0, "title": 1, "revenue": { "$multiply": ["$price", "$quantity"] } } },];let mut cursor = coll.aggregate(pipeline).await?;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.
Why stage order matters
Section titled “Why stage order matters”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.
Tips and gotchas
Section titled “Tips and gotchas”- An early
$matchcan use indexes; a$matchthat comes after$groupor$projectusually 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.