iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

$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.

Example

Example
// Explode array into one doc per element
{ $unwind: '$tags' }
Try it Yourself »

Discussion

Loading…