$unwind
$unwind flattens arrays into separate documents. Essential for joins with $lookup, per-element aggregation, and shape transformations.
MongoDB — $unwind
EXAMPLE
// ===== Sample doc =====
// {
// _id: 1,
// customer: 'Alex',
// items: [
// { sku: 'A', qty: 2 },
// { sku: 'B', qty: 1 },
// ]
// }
// ===== Basic =====
db.orders.aggregate([
{ $unwind: '$items' }
]);
// Result: 2 documents per original, each with one item:
// { _id: 1, customer: 'Alex', items: { sku: 'A', qty: 2 } }
// { _id: 1, customer: 'Alex', items: { sku: 'B', qty: 1 } }
// ===== Options =====
db.orders.aggregate([
{ $unwind: {
path: '$items',
preserveNullAndEmptyArrays: true, // keep docs with no array
includeArrayIndex: 'idx', // include element index
} }
]);
// preserveNullAndEmptyArrays: false (default) DROPS docs without array or empty array.
// ===== Aggregate per-element =====
db.orders.aggregate([
{ $unwind: '$items' },
{ $group: {
_id: '$items.sku',
total_qty: { $sum: '$items.qty' },
} }
]);
// ===== Unwind + lookup =====
db.orders.aggregate([
{ $unwind: '$items' },
{ $lookup: {
from: 'products',
localField: 'items.sku',
foreignField: 'sku',
as: 'product',
} },
{ $unwind: '$product' },
{ $project: { customer: 1, sku: '$items.sku', name: '$product.name', qty: '$items.qty' } }
]);
// ===== Nested unwind =====
db.orders.aggregate([
{ $unwind: '$items' },
{ $unwind: '$items.discounts' } // if discounts is array within items
]);
// ===== Re-group back into an array =====
db.orders.aggregate([
{ $unwind: '$items' },
{ $match: { 'items.qty': { $gt: 0 } } },
{ $group: {
_id: '$_id',
customer: { $first: '$customer' },
items: { $push: '$items' },
} }
]);
// Pattern: unwind, filter, regroup. Effectively filters elements within the array.
// ===== Performance =====
// Unwind multiplies documents. A 1M-doc collection with 10 items each becomes 10M virtual docs.
// Match BEFORE unwind whenever possible.
// Index on the array field helps the planner.
// ===== Patterns to internalise =====
// - Unwind to do per-element aggregation
// - Combine with $lookup for relational-style joins
// - preserveNullAndEmptyArrays when you must keep empty parents
// - Filter early; regroup after if you need the array back
// ===== Pitfalls =====
// - Unwind on a huge collection without prior match -> blast radius
// - Forgetting to handle docs where the array field is missing or null
// - Trying to update via aggregate output without merge stage
// - Multiple unwinds without thinking about the multiplicative effect
Why it matters
$unwind turns array fields into separate documents — the bridge between document and relational thinking. Pair with $lookup for joins, $group for per-element aggregation, and re-group when you want the array shape back. Match early; the document count multiplies fast.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…