$unwind Stage
$unwind Stage
Level 6 — Aggregation Framework The aggregation pipeline stage that deconstructs an array field from input documents, outputting one clone document for every individual element inside the array, enabling aggregation operations over array values.
1. Prerequisites
- Aggregation Pipeline (Concept) — The parent pipeline framework.
- Array — The array structure flattened.
2. Term Category
Aggregation (Array Deconstruction Pipeline Stage): The $unwind stage deconstructs an array field from input documents to output a document for each element in the array.
3. Explanation
Environment Context
- MongoDB Core (Executed in memory. Multiplies the number of documents in the pipeline stream, which temporarily increases the memory footprint during execution).
(1) Design Motivation — "Why did we design this?"
In relational database systems, tables are flat.
If you want to count tag popularity in SQL, you query a junction table where every row is already a single tag link.
In MongoDB, document fields can natively store list arrays:
{ name: "Sneakers", tags: ["shoes", "clothing"] }
If you try to run an aggregation query to count tag frequencies:
- If you group by
tags:{ $group: { _id: "$tags" } } - MongoDB groups by the entire array as a single value.
- It treats
["shoes", "clothing"]as a single group, separate from["shoes"], yielding incorrect tag counts.
We designed the $unwind stage to solve this array grouping problem.
It splits the array field.
For every element in the array, $unwind outputs a duplicate clone of the parent document, replacing the array field with that single element.
This flattens the data, allowing you to run standard $group operations on the individual elements.
(2) The Empty Array Gotcha
By default, if a document contains an empty array [], a null value, or is missing the array field entirely:
$unwindwill discard that document from the pipeline stream.- This can lead to missing data on reports.
To prevent this, you can pass the options format:
{ $unwind: { path: "$arrayField", preserveNullAndEmptyArrays: true } }
(3) Reality Metaphor (Trading Card Packs)
Imagine sorting trading cards:
- Input Document: A sealed Cardboard Wrapper containing 3 cards. You hold a single package.
$unwindStage: Tearing open the cardboard wrapper and laying the 3 individual cards side-by-side on the counter.- You now have 3 separate items to sort, count, or trade.
- The wrapper (the array shell) is gone, and the cards are flat.
(4) Code Examples
Splitting and Grouping Arrays (Unwind + Group)
Let's calculate the frequency of category tags across all products:
db.products.insertMany([
{ name: "Shirt", tags: ["clothing", "red"] },
{ name: "Jeans", tags: ["clothing", "blue"] }
]);
// Run aggregation
db.products.aggregate([
// 1. Unwind: Splits array, outputting 4 documents in the stream!
{
$unwind: "$tags" // Note the '$' prefix!
},
// 2. Group: Count the frequency of each tag
{
$group: {
_id: "$tags",
count: { $sum: 1 }
}
}
]);
// Output:
// { "_id": "clothing", "count": 2 }
// { "_id": "red", "count": 1 }
// { "_id": "blue", "count": 1 }
4. Common Mistakes & Pitfalls
Mistake 1: Forgetting that $unwind discards documents with empty or missing arrays by default, leading to missing data in reports
The mistake: Running a pipeline that unwinds a user's hobbies array to group them, only to discover that users who have no hobbies are completely missing from the final aggregate report.
Why it's wrong: The default behavior of $unwind filters out empty array paths.
If a document has hobbies: [] or is missing the hobbies key, it is purged from the pipeline stream, skewing your reporting totals.
Fix: Use the expanded object syntax with the preserveNullAndEmptyArrays: true option to keep documents with empty or missing arrays in the stream:
// CORRECT
db.users.aggregate([
{
$unwind: {
path: "$hobbies",
preserveNullAndEmptyArrays: true
}
}
]);
Mistake 2: Dropping Documents with Empty or Missing Array Fields when Using $unwind
The mistake: Running db.posts.aggregate([{ $unwind: "$tags" }]) expecting documents with tags: [] to remain.
Why it's wrong: By default, $unwind DROPS documents where the target array is null, missing, or empty []! Pass { preserveNullAndEmptyArrays: true } to keep empty array documents.
Incorrect:
db.posts.aggregate([{ $unwind: "$tags" }]); // ❌ Drops posts with empty tags array!
Fix:
db.posts.aggregate([{ $unwind: { path: "$tags", preserveNullAndEmptyArrays: true } }]);
Mistake 3: Forgetting $ Prefix in $unwind Field Path Parameter
The mistake: Executing db.posts.aggregate([{ $unwind: "tags" }]) without $ prefix.
Why it's wrong: $unwind strictly expects a field path string starting with $ (e.g. "$tags"). Omitting $ throws a stage parsing error.
Incorrect:
db.posts.aggregate([{ $unwind: "tags" }]); // ❌ Missing $ prefix!
Fix:
db.posts.aggregate([{ $unwind: "$tags" }]); // Correct $tags path
5. Practice Exercises
Exercise 1: Deconstructing Arrays for Per-Item Aggregation
Scenario:
Deconstruct the items array in orders collection to compute total quantities sold per product itemId.
Requirements:
- Use
$unwind: "$items". - Group by
$items.itemIdcomputing$sum: "$items.qty".
Answer
Implementation
db.orders.aggregate([
{ $unwind: "$items" },
{
$group: {
_id: "$items.itemId",
totalQuantitySold: { $sum: "$items.qty" }
}
}
]);
Technical Explanation
$unwindoutputs a separate document for each element in the specified array field.- Multiplies document count by array length.
- Enables grouping and accumulating across array item properties.
Exercise 2: Preserving Null and Empty Arrays with preserveNullAndEmptyArrays
Scenario:
Unwind tags array on products, keeping products that have empty or missing tags arrays.
Requirements:
- Use
$unwind: { path: "$tags", preserveNullAndEmptyArrays: true }.
Answer
Implementation
db.products.aggregate([
{
$unwind: {
path: "$tags",
preserveNullAndEmptyArrays: true
}
}
]);
Technical Explanation
- By default,
$unwinddrops documents where the array is empty[],null, or missing. preserveNullAndEmptyArrays: truepreserves those documents, outputting them withnullarray values.- Behaves like an outer join during array expansion.
Exercise 3: Tracking Original Array Element Indexes with includeArrayIndex
Scenario:
Include the original zero-indexed array position when unwinding a user's priorities list.
Requirements:
- Use
$unwindwithincludeArrayIndex: "arrayIndex".
Answer
Implementation
db.users.aggregate([
{
$unwind: {
path: "$priorities",
includeArrayIndex: "priorityOrder"
}
}
]);
Technical Explanation
includeArrayIndexinjects a new field containing the original zero-based element index into each output document.- Retains original array ordering metadata after array deconstruction.
- Useful for order-sensitive array analytics.
6. Related Terms
- Aggregation Pipeline (Concept) — The parent pipeline framework.
- Array — The data structure.
$lookupStage — Related concept:$lookupStage.
7. Key Takeaways
$unwindflattens BSON array fields into individual stream documents.- Creates one clone document copy per array element.
- Essential for running
$groupcalculations on array values. - Always prefix the target array path with
$(e.g."$tags"). - Empty arrays
[]or missing fields are discarded by default. - Set
preserveNullAndEmptyArrays: trueto prevent data loss. - Temporarily increases document count during processing.