$unwind
$unwind deconstructs an array field, producing one output document per array element. It's the pipeline stage you reach for whenever you need to treat array elements as independent rows — most commonly right before a $group that tallies something across those elements.
Basic Flattening
One document becomes many
db.posts.insertOne({ _id: 1, title: "Intro to MongoDB", tags: ["mongodb", "nosql", "database"] })
db.posts.aggregate([{ $unwind: "$tags" }])[
{ _id: 1, title: 'Intro to MongoDB', tags: 'mongodb' },
{ _id: 1, title: 'Intro to MongoDB', tags: 'nosql' },
{ _id: 1, title: 'Intro to MongoDB', tags: 'database' }
]Notice tags is no longer an array in the output — each document gets exactly one scalar value from the original array, with every other field copied unchanged.
preserveNullAndEmptyArrays
By default, $unwind drops any document where the array field is missing, null, or an empty array — there's nothing to "unwind" into. Set preserveNullAndEmptyArrays: true to keep those documents instead, with the field set to null.
Keeping documents with no array
db.posts.aggregate([
{
$unwind: {
path: "$tags",
preserveNullAndEmptyArrays: true
}
}
])
// A post with tags: [] or no "tags" field at all now appears once,
// with tags: null, instead of disappearing from the results.preserveNullAndEmptyArrays, documents lacking the array field silently vanish from the pipeline. This is a common source of "missing data" bug reports — always consider whether a document with an empty/absent array should still count toward downstream totals.includeArrayIndex
Add includeArrayIndex to capture the original position of each element — useful when order matters downstream (e.g. reconstructing rank, or the original array position for debugging).
Capturing the original array index
db.posts.aggregate([
{
$unwind: {
path: "$tags",
includeArrayIndex: "tagIndex"
}
}
])
// { _id: 1, tags: "mongodb", tagIndex: 0 }
// { _id: 1, tags: "nosql", tagIndex: 1 }
// { _id: 1, tags: "database",tagIndex: 2 }The Unwind + Group Pattern: Tag Counts
The single most common use of $unwind is immediately followed by $group — flatten an array, then tally something per element. This is how you compute a tag cloud, a "top categories" list, or per-item revenue from an order's line items.
Counting how often each tag appears across all posts
db.posts.aggregate([
{ $unwind: "$tags" },
{ $group: { _id: "$tags", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
])
// [ { _id: "mongodb", count: 45 }, { _id: "nosql", count: 30 }, ... ]Revenue per product across every order's line items
db.orders.aggregate([
{ $unwind: "$items" },
{
$group: {
_id: "$items.sku",
totalRevenue: { $sum: { $multiply: ["$items.qty", "$items.price"] } },
unitsSold: { $sum: "$items.qty" }
}
},
{ $sort: { totalRevenue: -1 } }
])Performance and Document-Explosion Warning
$unwind multiplies the number of documents flowing through the pipeline by the array's length — a 10,000-document collection where each document has a 50-element array becomes 500,000 intermediate documents. This can blow past memory limits in later stages and slow the pipeline dramatically. Filter with an early $match before unwinding whenever possible, and unwind only the array you actually need.Filter before you unwind
db.orders.aggregate([
{ $match: { status: "shipped", createdAt: { $gte: ISODate("2024-01-01") } } }, // shrink first
{ $unwind: "$items" }, // now unwinding a much smaller set of documents
{ $group: { _id: "$items.sku", unitsSold: { $sum: "$items.qty" } } }
])Alternatives That Avoid Unwinding
If you don't actually need one-document-per-element — you just need to compute something across the array — the array expression operators ($map, $filter, $reduce) can often do the job without exploding the document count at all.
Computing order total without $unwind
db.orders.aggregate([
{
$project: {
total: {
$sum: {
$map: {
input: "$items",
as: "item",
in: { $multiply: ["$$item.qty", "$$item.price"] }
}
}
}
}
}
])
// One output document per input document — no explosion, no $unwind neededApproach | Document Count | Best For |
|---|---|---|
$unwind + $group | Explodes, then collapses across the WHOLE collection | Aggregating a value ACROSS all documents (tag counts, per-SKU revenue) |
$map / $filter / $reduce | Stays at one document per input document | Computing a value WITHIN each document (e.g. that order's own total) |
$unwind + $group; the second is almost always cheaper with $map/$filter/$reduce and no $unwind at all.$unwindproduces one document per array element, copying every other field unchanged.preserveNullAndEmptyArrays: truekeeps documents whose array is missing/empty instead of dropping them.includeArrayIndexcaptures each element's original position.Unwind + group is the standard pattern for cross-document array aggregation (tag counts, per-item revenue).
Filter with
$matchbefore$unwindto avoid exploding a large array across a large collection.