4.1 Pipeline Fundamentals & Core Stages ($match, $project, $addFields)
Key Takeaways
- The Aggregation Framework processes documents through an ordered multi-stage stream pipeline where the output of each stage serves as the direct input to the next.
- Placing '$match' as early as possible maximizes query performance by leveraging B-tree indexes and reducing the working set before transformations.
- '$project' reshapes documents via inclusions ('1') or exclusions ('0') and expression calculation; inclusions and exclusions cannot be mixed except for '_id: 0'.
- '$addFields' (and its alias '$set') appends new computed fields or overwrites existing fields while preserving all other existing document fields intact.
- Each aggregation stage has a strict 100 MB RAM limit, failing with a memory limit exception unless '{ allowDiskUse: true }' is specified to enable temporary disk spill.
4.1 Pipeline Fundamentals & Core Stages ($match, $project, $addFields)
The MongoDB Aggregation Framework is a declarative, multi-stage data processing pipeline designed for advanced analytics, real-time data transformations, and reporting. While standard CRUD query methods (find() and findOne()) excel at simple document retrieval, filtering, and basic sorting, the Aggregation Framework allows developers to express complex data transformations, multi-document computations, and reshaping operations directly on the database server.
Understanding the mechanics of pipeline stage execution, index utilization, document projection, field manipulation, and memory management is a critical core competency for the MongoDB Certified Associate Developer Exam.
1. The Aggregation Pipeline Architecture
The Aggregation Framework operates on the concept of a data processing pipeline, analogous to Unix shell pipelines (cat data.txt | grep 'error' | awk '{print $2}' | sort).
+-----------------------------------------------------------------------------------------+
| MongoDB Aggregation Pipeline |
| |
| [ Collection Docs ] ---> [ Stage 1: $match ] ---> [ Stage 2: $project ] ---> [ Result ]|
| (Filter) (Reshape) |
+-----------------------------------------------------------------------------------------+
Core Pipeline Characteristics
- Document Stream Processing: Documents enter the pipeline sequentially from the source collection. Each stage receives a stream of BSON documents, applies its transformation or filter, and outputs a transformed stream of BSON documents to the subsequent stage.
- Stage Independence & Composability: Each stage is an independent operation defined by a stage operator object (e.g.,
{ $match: { ... } },{ $project: { ... } }). Stages can appear multiple times in a single pipeline, and their ordering dictates the precise sequence of execution. - Non-Destructive Execution: Unless an explicit materialization stage (
$outor$merge) is appended at the very end of the pipeline, aggregation operations do not alter the underlying data in the source collection. They return a transient cursor over the computed result set. - Typing & BSON Expression Support: Aggregation expressions can perform mathematical computations, string manipulation, date arithmetic, conditional branching (
$cond,$switch), and array filtering on document fields.
Basic Syntax in mongosh
db.collection.aggregate([
{ <stage 1> },
{ <stage 2> },
{ <stage 3> }
], { <pipeline options> });
2. Stage Ordering & The Pipeline Optimizer
The sequence of stages in an aggregation pipeline directly determines both the correctness of the final output and the runtime execution efficiency.
Why Stage Order Matters
Because each stage acts solely on the documents emitted by the immediately preceding stage, altering stage order changes the intermediate schema and dataset size:
- If a
$projectstage removes thestatusfield, a subsequent$matchstage filtering on{ status: "ACTIVE" }will evaluate againstnullvalues and produce empty or incorrect results. - If a
$sortstage is placed before a$matchstage, MongoDB must sort all documents in the collection before filtering out unwanted records, wasting substantial CPU and memory.
Automatic Pipeline Optimization
MongoDB includes an internal Aggregation Pipeline Optimizer that attempts to reorganize and coalesce stages before execution to optimize performance:
+---------------------------------------------------------------------------------+
| Pipeline Optimization Behaviors |
| |
| 1. $match Coalescing : Adjacent $match stages combine into one $and clause|
| 2. $match Pushdown : $match moves before $sort or $project if possible |
| 3. $project Coalescing : Adjacent $project stages merge into a single stage |
| 4. $limit / $skip Fusion : Adjacent $sort and $limit combine to Top-N sort |
+---------------------------------------------------------------------------------+
Best Practice Rule: Even though the optimizer performs stage pushdown where safe, developers should always explicitly place
$matchand$sortstages at the very beginning of the pipeline to guarantee optimal index utilization.
3. The $match Stage: Early Filtering & Index Utilization
The $match stage filters the document stream, allowing only documents matching the specified predicate to proceed to the next stage. It uses the exact same query syntax as the standard find() query filter.
Syntax
{ $match: { <query predicate> } }
Index Usage Mechanics
$match can leverage B-tree indexes to avoid full collection scans (COLLSCAN), but strict positional rules apply:
- First Stage Execution: When
$matchis the first stage in an aggregation pipeline, MongoDB uses standard index scans (IXSCAN) just likedb.collection.find(). - Post-Transformation Invalidation: If
$matchappears after any stage that modifies the document structure (such as$project,$group,$unwind, or$addFields), it can no longer use indexes on the original collection because the stream now contains in-memory transformed documents.
// OPTIMAL: Uses compound index on { status: 1, orderDate: -1 }
db.orders.aggregate([
{
$match: {
status: "DELIVERED",
orderDate: { $gte: ISODate("2026-01-01T00:00:00Z") }
}
},
{
$project: {
customerId: 1,
totalAmount: 1,
_id: 0
}
}
]);
// SUBOPTIMAL: $project precedes $match, preventing index utilization
db.orders.aggregate([
{
$project: {
status: 1,
orderDate: 1,
totalAmount: 1
}
},
{
$match: {
status: "DELIVERED" // Must perform in-memory scan of projected documents
}
}
]);
4. The $project Stage: Reshaping & Computing Projections
The $project stage reshapes each document in the stream by including existing fields, excluding fields, renaming fields, and creating entirely new computed fields using aggregation expressions.
Syntax
{
$project: {
<field1>: <1 | 0 | true | false | <expression>>,
<field2>: <1 | 0 | true | false | <expression>>
}
}
Inclusion vs. Exclusion Rules
- Inclusion Mode: Specifying fields with
1(ortrue) includes only those fields. All unspecified fields (except_id) are automatically suppressed from the output. - Exclusion Mode: Specifying fields with
0(orfalse) excludes those specific fields, returning all other fields present in the input document. - The Mixed Projection Prohibition: You cannot mix
1and0in the same$projectstage, with exactly one exception: the_idfield.
// VALID: Explicit inclusions + suppression of default _id
db.users.aggregate([
{
$project: {
username: 1,
email: 1,
_id: 0 // Allowed: _id is the only field that can be excluded during inclusion
}
}
]);
// VALID: Explicit exclusions
db.users.aggregate([
{
$project: {
passwordHash: 0,
internalNotes: 0
}
}
]);
// INVALID: Throws MongoServerError: Cannot do inclusion on field email in exclusion projection
// db.users.aggregate([
// { $project: { username: 1, passwordHash: 0 } }
// ]);
Computing Fields and Expressions in $project
$project is not limited to showing and hiding fields; it can compute new fields using aggregation expression operators:
db.orders.aggregate([
{
$project: {
orderId: "$_id", // Field renaming
customerUpper: { $toUpper: "$customerName" }, // String operator
finalTotal: {
$subtract: [
{ $add: [ "$subtotal", "$taxAmount" ] },
{ $ifNull: [ "$discount", 0 ] }
]
}, // Arithmetic calculation
isHighValue: { $gte: [ "$subtotal", 500 ] }, // Boolean expression
_id: 0
}
}
]);
Exam Trap: In projection and aggregation expressions, referencing the value of an existing document field requires prefixing the field name with a dollar sign in quotes (e.g.,
"$subtotal"). Writingsubtotalwithout$treats the value as a literal string constant.
5. The $addFields and $set Stages: Preserving Document Shape
A common limitation of $project is that when you want to append one computed field to a document with 30 fields, you must explicitly list all 30 fields with 1 or else $project drops them.
To solve this, MongoDB provides $addFields (and its identical alias $set, introduced in MongoDB 4.2). These stages append new fields or overwrite existing fields while automatically preserving all other unspecified fields in the document.
Syntax Comparison
// Using $addFields
{ $addFields: { <newField1>: <expression1>, <existingFieldToOverwrite>: <expression2> } }
// Using $set (Exact functional alias to $addFields)
{ $set: { <newField1>: <expression1>, <existingFieldToOverwrite>: <expression2> } }
Practical Example: Adding Tax and Timestamp
db.invoices.aggregate([
{
$match: { status: "PENDING" }
},
{
$set: {
tax: { $multiply: [ "$subtotal", 0.0825 ] },
totalDue: { $multiply: [ "$subtotal", 1.0825 ] },
processedAt: "$$NOW" // System variable for current ISODate
}
}
]);
In this query, every original field in invoices (such as _id, invoiceNumber, customer, lineItems, paymentTerms) is retained in the output, while tax, totalDue, and processedAt are added.
Direct Comparison: $project vs. $addFields / $set
| Feature | $project | $addFields / $set |
|---|---|---|
| Unspecified Fields | Dropped / Excluded (in inclusion mode) | Preserved intact |
| Field Removal | Supported (via exclusion mode field: 0) | Not supported (use $unset instead) |
| Field Addition | Supported (must also specify all retained fields) | Supported naturally without re-listing |
| Field Renaming | Supported (newKey: "$oldKey") | Supported (creates new field; requires $unset for old) |
| Primary Use Case | Constructing exact DTO shapes / stripping fields | Enriching documents with calculated metrics |
6. Memory Limits & allowDiskUse
Each individual stage in a MongoDB aggregation pipeline operates under a strict memory ceiling to prevent runaway queries from exhausting server RAM.
The 100 MB In-Memory Limit
By default, any individual pipeline stage is allocated a maximum of 100 MB of RAM for working state data. Stages that process documents in a streaming fashion (like $match and $project) process documents one at a time and rarely exceed this limit. However, blocking stages that must buffer multiple documents before emitting results—such as $sort (without an index), $group, and $bucket—can easily exceed 100 MB when processing large collections.
MongoServerError: PlanExecutor error during aggregation :: caused by ::
Sort exceeded memory limit of 104857600 bytes, but did not opt in to external sorting.
Aborting operation. Pass allowDiskUse: true to opt in.
Enabling Temporary Disk Spill: allowDiskUse: true
To allow memory-intensive pipeline stages to write temporary data files to the _tmp directory on the server disk, pass { allowDiskUse: true } in the aggregation options:
db.largeLogs.aggregate([
{
$match: { level: "ERROR" }
},
{
$group: {
_id: "$sourceIp",
errorCount: { $sum: 1 },
rawEvents: { $push: "$$ROOT" }
}
},
{
$sort: { errorCount: -1 }
}
], {
allowDiskUse: true // Permits pipeline stages to spill beyond 100 MB RAM to disk
});
Memory Limit Nuances & Atlas Restrictions
- In Atlas Shared Tier (M0, M2, M5) clusters,
allowDiskUse: trueis not permitted or severely restricted, making early filtering with$matchand index-backed sorting critical. - Document size limits remain unchanged: no individual output document emitted by any aggregation stage can ever exceed the 16 MB BSON document size limit.
A developer writes an aggregation pipeline on a collection of 5 million orders. The goal is to filter active orders, calculate a 10% discount field, and sort by order total. Which stage ordering allows MongoDB to utilize an existing index on '{ status: 1 }' for optimal performance?
What is the expected outcome when executing the following aggregation stage: 'db.inventory.aggregate([{ $project: { sku: 1, description: 1, costPrice: 0 } }])'?
A developer needs to add a calculated field 'margin' to product documents containing 25 existing attributes. The application must retain all 25 original fields without listing them manually in the aggregation query. Which stage should be used?
An un-indexed in-memory '$sort' and '$group' aggregation pipeline fails on a 50 GB collection with the error 'Stage exceeded memory limit of 104857600 bytes'. What configuration option resolves this runtime exception?