$match and $project
$match and $project are the two workhorse stages you'll use in nearly every pipeline: $match filters documents, and $project reshapes them. Getting both right — and knowing when $addFields is a better fit than $project — is foundational to writing efficient aggregations.
$match — Filter Early
$match uses the exact same query operator syntax as find()'s filter argument. Placed as the first stage, it can use an index exactly like a normal query — placed later, it can only filter documents already materialized by earlier stages.
$match as the first stage
db.orders.aggregate([
{ $match: { status: "shipped", total: { $gte: 50 } } },
// ... later stages only see documents that passed this filter
])$match run earlier?" A $match right after the pipeline starts can use an index; the same $match placed after a $group or $unwind cannot, because the documents at that point are pipeline output, not the original indexed collection.$project — Shaping Output
$project controls exactly which fields appear in the output — and, unlike a find() projection, it can also compute new fields from expressions.
Include, exclude, and compute in one $project
db.orders.aggregate([
{
$project: {
_id: 0, // exclude _id
customer: 1, // include as-is
total: 1, // include as-is
totalWithTax: { $multiply: ["$total", 1.13] } // computed field
}
}
])find() projection, a $project stage generally cannot mix plain inclusion (1) and exclusion (0) for different fields in the same stage — except _id, which can always be excluded from an otherwise-inclusion projection.$addFields — Add Without Losing Everything Else
$addFields (an alias of $set in the aggregation context) adds or overwrites specific fields while passing every other existing field through unchanged — the opposite default of $project, which drops anything not explicitly listed.
$addFields keeps everything else
db.orders.aggregate([
{ $addFields: { totalWithTax: { $multiply: ["$total", 1.13] } } }
])
// Every original field (customer, status, items, ...) is STILL present,
// plus the new totalWithTax field.Stage | Default Behavior | Best For |
|---|---|---|
$project | Only listed fields survive (unless in exclusion mode) | Trimming a document down to exactly the fields the client needs |
$addFields | All existing fields survive, plus whatever you add | Adding a computed field without having to re-list every other field |
Expression Operators in Projections
The value side of a $project/$addFields field can be any aggregation expression — string, arithmetic, date, conditional, and array operators are all fair game.
String and arithmetic expressions
db.users.aggregate([
{
$project: {
fullNameUpper: { $toUpper: { $concat: ["$firstName", " ", "$lastName"] } },
ageNextYear: { $add: ["$age", 1] },
discountedPrice: { $multiply: ["$price", { $subtract: [1, "$discountRate"] }] }
}
}
])Conditional expressions
db.orders.aggregate([
{
$project: {
total: 1,
tier: {
$cond: {
if: { $gte: ["$total", 100] },
then: "high-value",
else: "standard"
}
}
}
}
])Renaming Fields
There's no $rename stage in aggregation — renaming is just projecting the old field's value under a new name (and excluding the old one, if using inclusion mode).
Renaming via $project
db.legacyUsers.aggregate([
{
$project: {
_id: 0,
fullName: "$name", // "name" becomes "fullName"
emailAddress: "$email" // "email" becomes "emailAddress"
}
}
])Combining $match and $project
A realistic two-stage pipeline
db.orders.aggregate([
{ $match: { status: "shipped", createdAt: { $gte: ISODate("2024-01-01") } } },
{
$project: {
_id: 0,
orderId: "$_id",
customer: 1,
total: 1,
totalWithTax: { $round: [{ $multiply: ["$total", 1.13] }, 2] }
}
}
])[
{ customer: 'Alice', total: 42.5, orderId: ObjectId('66a1...'), totalWithTax: 48.03 },
{ customer: 'Carol', total: 88, orderId: ObjectId('66a2...'), totalWithTax: 99.44 }
]$matchuses the same operators asfind()— put it as early as possible so it can use an index.$projectdrops any field you don't explicitly list (except_id); use it to shape a lean API response.$addFieldskeeps every existing field and layers new/overwritten ones on top — use it when you only need to compute one or two extra fields.Renaming a field in aggregation is just projecting
newName: "$oldName".