$unwind — flattening arrays
Arrays are everywhere in MongoDB documents — tags on a post, items in an order, lessons in a course. To summarise across the elements of those arrays, you usually need to look at them one at a time. $unwind does exactly that: it takes a document with an array field and emits one document per element, copying the rest of the document onto each. A document with a three-element array becomes three documents.
For this lesson each sales document records an order with several line items in an items array:
{ "_id": "o1", "buyer": "Ada", "items": [ { "title": "Dune", "qty": 2 }, { "title": "Foundation", "qty": 1 } ]}One document per array element
Section titled “One document per array element”Give $unwind the path to an array field (with the $ prefix), and it fans the document out. Every emitted document is identical except that the array field is replaced by a single one of its elements.
flowchart LR In["one order with items array of two"] --> U["$unwind items"] U --> D1["order, items = Dune"] U --> D2["order, items = Foundation"]
db.sales.aggregate([ { $unwind: "$items" }])const rows = await db.collection("sales").aggregate([ { $unwind: "$items" },]).toArray();rows = list(db.sales.aggregate([ {"$unwind": "$items"},]))pipeline := mongo.Pipeline{ {{"$unwind", "$items"}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$unwind": "$items" },];let mut cursor = coll.aggregate(pipeline).await?;The single order becomes one document per line item, with items now a plain object rather than an array:
[ { "_id": "o1", "buyer": "Ada", "items": { "title": "Dune", "qty": 2 } }, { "_id": "o1", "buyer": "Ada", "items": { "title": "Foundation", "qty": 1 } }]In Compass: add an $unwind stage in the Aggregations tab and point it at the array field. The preview shows the document count grow — a sign you have flattened an array — which is the opposite of what $match and $group do.
The classic pairing: $unwind then $group
Section titled “The classic pairing: $unwind then $group”$unwind is rarely the last word. Its usual job is to flatten an array so a later $group can summarise across the elements. Here we flatten every order’s items, then total the quantity sold per title across all orders:
db.sales.aggregate([ { $unwind: "$items" }, { $group: { _id: "$items.title", sold: { $sum: "$items.qty" } } }, { $sort: { sold: -1 } }])const rows = await db.collection("sales").aggregate([ { $unwind: "$items" }, { $group: { _id: "$items.title", sold: { $sum: "$items.qty" }, } }, { $sort: { sold: -1 } },]).toArray();rows = list(db.sales.aggregate([ {"$unwind": "$items"}, {"$group": { "_id": "$items.title", "sold": {"$sum": "$items.qty"}, }}, {"$sort": {"sold": -1}},]))pipeline := mongo.Pipeline{ {{"$unwind", "$items"}}, {{"$group", bson.D{ {"_id", "$items.title"}, {"sold", bson.D{{"$sum", "$items.qty"}}}, }}}, {{"$sort", bson.D{{"sold", -1}}}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$unwind": "$items" }, doc! { "$group": { "_id": "$items.title", "sold": { "$sum": "$items.qty" } } }, doc! { "$sort": { "sold": -1 } },];let mut cursor = coll.aggregate(pipeline).await?;Now each title has its total quantity, summed across every order’s line items:
[ { "_id": "Dune", "sold": 5 }, { "_id": "Foundation", "sold": 3 }]Keeping empty and missing arrays
Section titled “Keeping empty and missing arrays”By default, $unwind drops a document whose array field is empty, missing, or null — there is no element to emit, so the document vanishes. When you want those documents to survive (with the field absent), pass the long form with preserveNullAndEmptyArrays:
db.sales.aggregate([ { $unwind: { path: "$items", preserveNullAndEmptyArrays: true } }])const rows = await db.collection("sales").aggregate([ { $unwind: { path: "$items", preserveNullAndEmptyArrays: true, } },]).toArray();rows = list(db.sales.aggregate([ {"$unwind": { "path": "$items", "preserveNullAndEmptyArrays": True, }},]))pipeline := mongo.Pipeline{ {{"$unwind", bson.D{ {"path", "$items"}, {"preserveNullAndEmptyArrays", true}, }}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$unwind": { "path": "$items", "preserveNullAndEmptyArrays": true } },];let mut cursor = coll.aggregate(pipeline).await?;An order with no items still appears once, simply without an items field:
[ { "_id": "o2", "buyer": "Grace" }]Tips and gotchas
Section titled “Tips and gotchas”$unwindmultiplies documents. An order with five items becomes five documents — flattening can grow the stream a lot before a later$groupshrinks it again.- Without
preserveNullAndEmptyArrays, documents with an empty or missing array are silently dropped. If your counts come up short after an unwind, this is the usual culprit. - After unwinding, reference the element’s fields through the original path: once
itemsis a single object,"$items.qty"reads that element’s quantity. - If the field is not actually an array,
$unwindtreats a single scalar as a one-element array and emits the document once — so it is forgiving of non-array values.