MongoDB $unwind Stage

Beginner
⏱️ 11 min read
📚 Updated: Jul 2026
🎯 5 Examples
Aggregation

What You’ll Learn

The $unwind stage splits array fields into one document per element—essential for tag analytics, order line items, flattening $lookup results, and any pipeline that must treat array items as rows.

01

Array → rows

One per item.

02

path

Field to split.

03

preserve null

Keep empty.

04

Array index

Position.

05

+ $group

Count tags.

06

+ $lookup

Flatten join.

Definition and Usage

$unwind is an aggregation stage that takes an array field and outputs one document per array element. All other fields on the parent document are copied into each output row—the array field is replaced by a single scalar (or sub-document) value per row.

💡
Beginner tip

Before: { title: "Post A", tags: ["mongo", "api"] } — one document.
After { $unwind: "$tags" }: two documents — tags: "mongo" and tags: "api".
Same title on both rows; tags split for filtering or grouping.

Use $unwind whenever array elements need individual treatment: count tags with $group, sum line-item prices, match on one skill in a list, or flatten $lookup join arrays into readable rows.

📝 Syntax

$unwind supports a shorthand string path or an extended options document:

mongosh
// Shorthand
{ $unwind: <string path> }

// Extended
{
  $unwind: {
    path: <string path>,
    includeArrayIndex: <string>,       // optional
    preserveNullAndEmptyArrays: <boolean>  // optional, default false
  }
}

Syntax Rules

  • path — required. Prefixed with $: "$tags", "$items", "$lookupResult".
  • Default behavior — documents with missing, null, or empty arrays are excluded from output.
  • preserveNullAndEmptyArrays: true — keeps those documents; array field becomes null in output.
  • includeArrayIndex — adds a new field with the zero-based index of the element in the original array.
  • Non-array values — if the field holds a single value (not an array), MongoDB treats it as a one-element array.
  • Document count — multiplies rows: N elements → N output documents (per input document).
mongosh
db.articles.aggregate([
  { $unwind: "$tags" }
])

⚡ Quick Reference

QuestionAnswer
What it doesOne output document per array element
Shorthand{ $unwind: "$arrayField" }
Empty array defaultDocument dropped from output
Keep empty/nullpreserveNullAndEmptyArrays: true
Element indexincludeArrayIndex: "fieldName"
Common next stage$group, $match, $sortByCount
Basic
{ $unwind:
  "$tags"
}

Split tags

Preserve
preserveNullAndEmpty
Arrays: true

Keep empty

Index
includeArrayIndex:
"lineIdx"

Position

Pattern
$unwind →
$group

Tag counts

🧰 $unwind Options

Extended syntax gives control over empty arrays and element position:

path Required

Array field to deconstruct. Must be a string starting with $. Can be nested: "$order.items".

{ $unwind: "$skills" }
{ $unwind: { path: "$items" } }
preserveNullAndEmptyArrays Optional

Default false. When true, documents with missing, null, or empty arrays still appear in output once.

{ $unwind: {
  path: "$tags",
  preserveNullAndEmptyArrays: true
}}
includeArrayIndex Optional

String name for a new field holding the element’s zero-based index—useful for line numbers and ordered lists.

{ $unwind: {
  path: "$items",
  includeArrayIndex: "lineIndex"
}}
After $lookup Pattern

Join results arrive in an array. Unwind to flatten—one parent row per matched foreign document.

{ $lookup: { ... } },
{ $unwind: "$customerData" }

Examples Gallery

Articles and orders—basic tag split, optional tags with preserve, line items with index, tag frequency via $group, and flattening lookup-style arrays.

📚 Getting Started

Articles with tags and orders with line items.

Example 1 — Articles and orders collections

mongosh
db.articles.drop()
db.orders.drop()

db.articles.insertMany([
  { title: "MongoDB Basics",    tags: ["mongodb", "database"] },
  { title: "REST API Guide",    tags: ["nodejs", "api"]       },
  { title: "Draft Notes",       tags: []                      },
  { title: "Untagged Post",     tags: null                    }
])

db.orders.insertMany([
  {
    orderId: "O-1001",
    customer: "Ana",
    items: [
      { product: "Widget", qty: 2, price: 25 },
      { product: "Cable",  qty: 1, price: 9  }
    ]
  },
  {
    orderId: "O-1002",
    customer: "Ben",
    items: [
      { product: "Gadget", qty: 1, price: 80 }
    ]
  }
])

How It Works

Articles include normal tags, an empty array, and null—perfect for demonstrating default vs preserve behavior. Orders have nested item arrays for index examples.

Example 2 — Unwind tags (basic shorthand)

mongosh
db.articles.aggregate([
  { $unwind: "$tags" },
  {
    $project: {
      _id: 0,
      title: 1,
      tag: "$tags"
    }
  }
])

// "MongoDB Basics" (2 tags) → 2 rows
// "REST API Guide" (2 tags)  → 2 rows
// "Draft Notes" (empty)      → DROPPED (default)
// "Untagged Post" (null)     → DROPPED (default)
// Total: 4 output documents

How It Works

The shorthand { $unwind: "$tags" } is the most common form. Each tag becomes its own row while title repeats on every row from the same article.

Example 3 — Keep articles with no tags (preserveNullAndEmptyArrays)

mongosh
db.articles.aggregate([
  {
    $unwind: {
      path: "$tags",
      preserveNullAndEmptyArrays: true
    }
  },
  {
    $project: {
      _id: 0,
      title: 1,
      tag: "$tags"
    }
  }
])

// Same 4 tagged rows as Example 2, PLUS:
// "Draft Notes"   → tag: null (empty array preserved)
// "Untagged Post" → tag: null (null array preserved)
// Total: 6 output documents

How It Works

Use preserveNullAndEmptyArrays: true when untagged or empty-list documents must still appear—optional categories, incomplete records, or left-outer-join semantics after lookup.

📈 Practical Patterns

Line indexes, tag analytics, and lookup flattening.

Example 4 — Order line items with array index

mongosh
db.orders.aggregate([
  {
    $unwind: {
      path: "$items",
      includeArrayIndex: "lineIndex"
    }
  },
  {
    $project: {
      _id: 0,
      orderId: 1,
      customer: 1,
      lineIndex: 1,
      product: "$items.product",
      qty: "$items.qty",
      price: "$items.price",
      lineTotal: {
        $multiply: ["$items.qty", "$items.price"]
      }
    }
  }
])

// O-1001 → 2 rows: lineIndex 0 (Widget), lineIndex 1 (Cable)
// O-1002 → 1 row:  lineIndex 0 (Gadget)

How It Works

includeArrayIndex preserves position—line 1, line 2 on invoices—or lets you sort items in original order after unwind.

Example 5 — Tag frequency ($unwind + $group)

mongosh
// Count how many articles use each tag
db.articles.aggregate([
  { $match: { tags: { $exists: true, $ne: null, $not: { $size: 0 } } } },
  { $unwind: "$tags" },
  {
    $group: {
      _id: "$tags",
      articleCount: { $sum: 1 }
    }
  },
  { $sort: { articleCount: -1 } },
  {
    $project: {
      _id: 0,
      tag: "$_id",
      articleCount: 1
    }
  }
])

// mongodb & database: 1 each (MongoDB Basics)
// nodejs & api: 1 each (REST API Guide)
// Shortcut alternative: $unwind then { $sortByCount: "$tags" }

How It Works

The classic analytics chain: unwind array elements into rows, then group and count. Same pattern powers tag clouds, skill inventories, and category breakdowns. For count-only output, $sortByCount after unwind is a one-stage shortcut.

🧠 How $unwind Works

1

Read array field

MongoDB reads the field at path on each input document.

Input
2

Emit one row per element

Each array item becomes a separate document; sibling fields are copied into every row.

Split
3

Apply options

Empty/null arrays drop or preserve per preserveNullAndEmptyArrays; index added if requested.

Options
=

Flattened stream

Downstream stages see scalar array values—ready for $match, $group, or joins.

📝 Notes

  • $unwind multiplies document count—large arrays increase pipeline volume.
  • Empty or missing arrays exclude documents unless preserveNullAndEmptyArrays: true.
  • Path must be a string with $ prefix: "$fieldName".
  • After $lookup, unwind the join array field to avoid nested array results.
  • For tag/category frequency, chain $unwind$sortByCount or $group.
  • Previous topic: $unset. Next: $group.

Conclusion

$unwind turns array fields into rows: use shorthand for simple splits, preserveNullAndEmptyArrays for optional arrays, and includeArrayIndex when position matters.

Pair with $group for analytics or follow $lookup to flatten joins. Next: $group to bucket and summarize unwound rows.

💡 Best Practices

✅ Do

  • Unwind before $group when counting array element frequencies
  • Unwind $lookup result arrays for flat joined reports
  • Use preserveNullAndEmptyArrays: true for optional or incomplete arrays
  • Add includeArrayIndex for ordered line items or ranked lists
  • $match before unwind when only some parent documents matter

❌ Don’t

  • Unwind huge arrays on every request without filtering first
  • Forget that empty arrays disappear with default settings
  • Skip unwind and expect $group to count individual tags correctly
  • Unwind the same array twice without need—row count grows fast
  • Omit the $ prefix on the path string

Key Takeaways

Knowledge Unlocked

Five things to remember about $unwind

Use these points whenever array elements must become rows.

5
Core concepts
📝 02

$path

Field string.

Syntax
📋 03

Preserve empty

Optional flag.

Option
🔢 04

Index field

Line number.

Order
👥 05

+ $group

Analytics.

Pattern

❓ Frequently Asked Questions

$unwind deconstructs an array field so each array element becomes its own output document. One document with tags: ["mongo", "api"] becomes two documents—each with a single tag value—so you can filter, group, or join on individual array items.
Short form: { $unwind: "$arrayField" }. Extended form: { $unwind: { path: "$arrayField", preserveNullAndEmptyArrays: true, includeArrayIndex: "indexName" } }. The path must be prefixed with $.
By default, $unwind removes documents where the array is missing, null, or empty. Set preserveNullAndEmptyArrays: true to keep those documents with the array field null or missing in output.
When you need counts or sums per array element—tag frequency, line-item totals, skills per employee—you must unwind first so each element is its own row, then $group on that field.
$lookup returns matching documents in an array field. $unwind flattens that array so joined fields sit at the top level—one output row per match instead of nested arrays.
Yes. Set includeArrayIndex to a field name string—e.g. includeArrayIndex: "lineIndex"—and MongoDB adds the zero-based position of each element in the original array.
Did you know?

MongoDB also offers $unwind with a path pointing at nested arrays—you can unwind sub-documents inside arrays when each element is an object (like order line items). For arrays of arrays, you may need two unwind stages in sequence. See the official $unwind docs.

Continue the Stages Series

Flatten arrays with $unwind, then summarize with $group.

Next: $group →

About the author

Mari Selvan M P
Mari Selvan M P 🔗

Developer, cloud engineer, and technical writer

  • Experience 12 years building web and cloud systems
  • Focus Full Stack Development, AWS, and Developer Education

I write practical tutorials so students and working developers can learn by doing—from databases and APIs to deployment on AWS.

5 people found this page helpful