DocsHub
Aggregation

$unwind

Learn how to deconstruct array fields into separate documents using $unwind in MongoDB aggregation pipelines.

$unwind

Arrays are powerful in MongoDB — a student can have multiple subjects, multiple grades, multiple activities all stored in one document. But when you need to analyze array data — count how many students take each subject, find the average score per subject — working with arrays as a whole becomes difficult.

$unwind solves this. It takes a document with an array field and creates one separate document for each element in that array.


How $unwind Works

"One document{ name: 'Ali Hassan', subjects: ['Math', 'Physics', 'English' B Document 1{ name: 'Ali Hassan', subjects: 'Math'} Document 2{ name: 'Ali Hassan', subjects: 'Physics'} Document 3{ name: 'Ali Hassan', subjects: 'English'}

One document with 3 subjects becomes 3 separate documents — one per subject. All other fields stay the same. Only the array field changes — it becomes a single value instead of an array.


Syntax

{ $unwind: "$arrayField" }

Or the full syntax with options:

{
  $unwind: {
    path: "$arrayField",
    includeArrayIndex: "indexField",  // optional
    preserveNullAndEmpty: true        // optional
  }
}

Basic Example

Let's see $unwind on our students collection. Each student has a subjects array:

// Original document
{
  name: "Ali Hassan",
  grade: "10th",
  subjects: ["Math", "Physics", "English"]
}

Apply $unwind:

db.students.aggregate([
  { $match: { name: "Ali Hassan" } },
  { $unwind: "$subjects" }
])

Result:

[
  { _id: ObjectId("..."), name: "Ali Hassan", grade: "10th", subjects: "Math" },
  { _id: ObjectId("..."), name: "Ali Hassan", grade: "10th", subjects: "Physics" },
  { _id: ObjectId("..."), name: "Ali Hassan", grade: "10th", subjects: "English" }
]

Three documents — one for each subject. The _id is the same in all three because they came from the same original document.


Why $unwind is Useful — Count Students Per Subject

On its own, $unwind just expands documents. The real power comes when you combine it with $group.

Let's count how many students take each subject:

db.students.aggregate([
  // Stage 1 — only enrolled students
  { $match: { enrolled: true } },

  // Stage 2 — unwind subjects array
  // Each student now becomes multiple documents — one per subject
  { $unwind: "$subjects" },

  // Stage 3 — group by subject and count
  {
    $group: {
      _id: "$subjects",
      studentCount: { $sum: 1 }
    }
  },

  // Stage 4 — sort by most popular subject
  { $sort: { studentCount: -1 } },

  // Stage 5 — clean output
  {
    $project: {
      _id: 0,
      subject: "$_id",
      studentCount: 1
    }
  }
])

Result:

[
  { subject: "Math",             studentCount: 89 },
  { subject: "English",          studentCount: 85 },
  { subject: "Physics",          studentCount: 72 },
  { subject: "Biology",          studentCount: 61 },
  { subject: "Computer Science", studentCount: 48 }
]

Without $unwind, this would be impossible to calculate inside MongoDB. You would have to pull all student documents into your app and count in JavaScript.


$unwind with Arrays of Objects

$unwind works just as well with arrays of objects — like a student's grades array:

// Original document
{
  name: "Ali Hassan",
  grade: "10th",
  grades: [
    { subject: "Math",    score: 88, semester: 1 },
    { subject: "Physics", score: 92, semester: 1 },
    { subject: "English", score: 79, semester: 1 }
  ]
}
db.students.aggregate([
  { $match: { name: "Ali Hassan" } },
  { $unwind: "$grades" }
])

Result:

[
  { name: "Ali Hassan", grade: "10th", grades: { subject: "Math",    score: 88, semester: 1 } },
  { name: "Ali Hassan", grade: "10th", grades: { subject: "Physics", score: 92, semester: 1 } },
  { name: "Ali Hassan", grade: "10th", grades: { subject: "English", score: 79, semester: 1 } }
]

Now combine with $group to calculate average score per subject across all students:

db.students.aggregate([
  // Only enrolled students
  { $match: { enrolled: true } },

  // Unwind grades array
  { $unwind: "$grades" },

  // Group by subject — calculate average score
  {
    $group: {
      _id: "$grades.subject",
      averageScore: { $avg: "$grades.score" },
      highestScore: { $max: "$grades.score" },
      lowestScore:  { $min: "$grades.score" },
      totalStudents: { $sum: 1 }
    }
  },

  // Sort by average score descending
  { $sort: { averageScore: -1 } },

  // Clean output
  {
    $project: {
      _id: 0,
      subject: "$_id",
      averageScore: { $round: ["$averageScore", 1] },
      highestScore: 1,
      lowestScore: 1,
      totalStudents: 1
    }
  }
])

Result:

[
  { subject: "English", averageScore: 84.2, highestScore: 98, lowestScore: 52, totalStudents: 85 },
  { subject: "Math",    averageScore: 81.5, highestScore: 99, lowestScore: 44, totalStudents: 89 },
  { subject: "Physics", averageScore: 78.9, highestScore: 97, lowestScore: 38, totalStudents: 72 }
]

includeArrayIndex

When you need to know the position of each element in the original array, use includeArrayIndex:

db.students.aggregate([
  { $match: { name: "Ali Hassan" } },
  {
    $unwind: {
      path: "$subjects",
      includeArrayIndex: "subjectPosition"
    }
  }
])

Result:

[
  { name: "Ali Hassan", subjects: "Math",    subjectPosition: 0 },
  { name: "Ali Hassan", subjects: "Physics", subjectPosition: 1 },
  { name: "Ali Hassan", subjects: "English", subjectPosition: 2 }
]

subjectPosition tells you where each element was in the original array — 0 for first, 1 for second, and so on.


preserveNullAndEmpty

By default, $unwind removes documents where the array field is missing, null, or empty. Use preserveNullAndEmpty: true to keep those documents instead:

// Some students have no subjects assigned yet
{ name: "New Student", grade: "9th", subjects: [] }
{ name: "Another Student", grade: "9th" }  // no subjects field at all
// Default — these documents are removed by $unwind
db.students.aggregate([
  { $unwind: "$subjects" }
])
// New Student and Another Student do not appear in results

// With preserveNullAndEmpty — these documents are kept
db.students.aggregate([
  {
    $unwind: {
      path: "$subjects",
      preserveNullAndEmpty: true
    }
  }
])
// New Student appears with subjects: null
// Another Student appears with subjects: null

Use preserveNullAndEmpty: true when you need to keep track of documents that have no array elements — for example, when you want to report which students have not been assigned any subjects yet.


$unwind then $group — The Core Pattern

The most important pattern with $unwind is:

$match → $unwind → $group

Filter the documents you need, unwind the array to get individual elements, then group by those elements to calculate something.

// Most common subjects taken by 10th grade students
db.students.aggregate([
  { $match: { grade: "10th", enrolled: true } },
  { $unwind: "$subjects" },
  {
    $group: {
      _id: "$subjects",
      count: { $sum: 1 }
    }
  },
  { $sort: { count: -1 } },
  { $project: { _id: 0, subject: "$_id", count: 1 } }
])
// Average score per subject for students in semester 1
db.students.aggregate([
  { $match: { enrolled: true } },
  { $unwind: "$grades" },
  { $match: { "grades.semester": 1 } },
  {
    $group: {
      _id: "$grades.subject",
      averageScore: { $avg: "$grades.score" }
    }
  },
  { $sort: { averageScore: -1 } },
  { $project: { _id: 0, subject: "$_id", averageScore: { $round: ["$averageScore", 1] } } }
])

Notice the second $match after $unwind — you can filter on the unwound field values. This is a powerful combination.


School System Examples

// Count how many students take each subject — all grades
db.students.aggregate([
  { $match: { enrolled: true } },
  { $unwind: "$subjects" },
  { $group: { _id: "$subjects", count: { $sum: 1 } } },
  { $sort: { count: -1 } },
  { $project: { _id: 0, subject: "$_id", count: 1 } }
])

// Most popular subjects in 10th grade
db.students.aggregate([
  { $match: { grade: "10th", enrolled: true } },
  { $unwind: "$subjects" },
  { $group: { _id: "$subjects", count: { $sum: 1 } } },
  { $sort: { count: -1 } },
  { $limit: 3 }
])

// Average score per subject across all students
db.students.aggregate([
  { $match: { enrolled: true } },
  { $unwind: "$grades" },
  {
    $group: {
      _id: "$grades.subject",
      averageScore:  { $avg: "$grades.score" },
      highestScore:  { $max: "$grades.score" },
      lowestScore:   { $min: "$grades.score" }
    }
  },
  { $sort: { averageScore: -1 } },
  {
    $project: {
      _id: 0,
      subject: "$_id",
      averageScore: { $round: ["$averageScore", 1] },
      highestScore: 1,
      lowestScore: 1
    }
  }
])

// Find students who failed any subject (score below 50)
db.students.aggregate([
  { $match: { enrolled: true } },
  { $unwind: "$grades" },
  { $match: { "grades.score": { $lt: 50 } } },
  {
    $group: {
      _id: "$name",
      failedSubjects: { $push: "$grades.subject" },
      lowestScore: { $min: "$grades.score" }
    }
  },
  { $project: { _id: 0, student: "$_id", failedSubjects: 1, lowestScore: 1 } }
])

// How many subjects does each student take on average
db.students.aggregate([
  { $match: { enrolled: true } },
  { $unwind: "$subjects" },
  { $group: { _id: "$_id", name: { $first: "$name" }, subjectCount: { $sum: 1 } } },
  { $group: { _id: null, averageSubjects: { $avg: "$subjectCount" } } },
  { $project: { _id: 0, averageSubjects: { $round: ["$averageSubjects", 1] } } }
])

Quick Reference

SyntaxWhat it does
{ $unwind: "$field" }Unwind array — removes docs with missing or empty array
{ $unwind: { path: "$field", includeArrayIndex: "idx" } }Unwind and include position of each element
{ $unwind: { path: "$field", preserveNullAndEmpty: true } }Unwind and keep documents with missing or empty array

The pattern $unwind followed by $group is one of the most powerful combinations in MongoDB aggregation. Any time you need to analyze what is inside your arrays — count values, calculate averages, find totals — reach for this pattern. Unwind first to flatten the array, then group to calculate what you need.

On this page