MongoDB$match & $project Stages

$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

JS
db.orders.aggregate([
  { $match: { status: "shipped", total: { $gte: 50 } } },
  // ... later stages only see documents that passed this filter
])
Tip
Always ask "could this $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

JS
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
    }
  }
])
Warning
Just like a 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

JS
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

JS
db.users.aggregate([
  {
    $project: {
      fullNameUpper: { $toUpper: { $concat: ["$firstName", " ", "$lastName"] } },
      ageNextYear: { $add: ["$age", 1] },
      discountedPrice: { $multiply: ["$price", { $subtract: [1, "$discountRate"] }] }
    }
  }
])

Conditional expressions

JS
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

JS
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

JS
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 }
]
  • $match uses the same operators as find() — put it as early as possible so it can use an index.

  • $project drops any field you don't explicitly list (except _id); use it to shape a lean API response.

  • $addFields keeps 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".