✏ MongoDB Aggregation: $unwind stage
The $unwind
stage is meant to handle fields that have arrays as values. It often comes before the $group
stage, and it'll finally solve the problem I had with my recipe database - the reason why I started looking into Mongo aggregation in the first place.
Here's my collection again:
Each recipe has a list of ingredients: An array of objects holding the ingredient's name, amount and unit, and my goal was to extract a list of all distinct ingredient names from the recipe collection.
I could've fetched the whole collection and processed it on the server with JavaScript Array methods to achieve that, but I wanted to know how to get this result with only one query to Mongo.
✏ Syntax
To $unwind
(= destructure) an array field, pass it like this:
Recipe.aggregate([
{ $unwind: '$ingredients' }
]);
$unwind
increases the number of documents. If the array has 3 items, the result will be 3 documents, where the array field has now the value of each item.
✏ Example
For a first example, I'll start with a $match
stage, so I'm only working on one recipe, and a $project
stage, to reduce the number of fields in the output:
Recipe.aggregate([
{ $match: { title: 'Soup' } },
{ $project: { _id: 0, title: 1, ingredients: '$ingredients.name' } }
]);
// result
[ { title: 'Soup', ingredients: [ 'tomatos', 'salt', 'pepper' ] ]
The result is the "Soup" document with only the title and an array of the ingredients' names.
Adding $unwind
will transform that into 3 documents, where all fields have the same values, with the exception of the ingredients field:
Recipe.aggregate([
{ $match: { title: 'Soup' } },
{ $project: { _id: 0, title: 1, ingredients: '$ingredients.name' } },
{ $unwind: '$ingredients' }
]);
// result
[
{ title: 'Soup', ingredients: 'tomatos' },
{ title: 'Soup', ingredients: 'salt' },
{ title: 'Soup', ingredients: 'pepper' }
]
If I remove the $match
stage to work on the whole collection, and also exclude the title in the $project
stage, I get this result:
[
{ ingredients: 'tomatos' },
{ ingredients: 'salt' },
{ ingredients: 'pepper' },
{ ingredients: 'spaghetti' },
{ ingredients: 'salt' },
{ ingredients: 'pesto' },
{ ingredients: 'tomatos' },
{ ingredients: 'oil' },
{ ingredients: 'pepper' },
{ ingredients: 'tomatos' }
]
This is already quite close to what I wanted to achieve, but I have many duplicates. The solution is to add a $group
stage now:
✏ $unwind in conjunction with $group
Since $group
only returns distinct documents, it'll filter out all duplicates for me:
Recipe.aggregate([
{ $project: { _id: 0, ingredients: '$ingredients.name' } },
{ $unwind: '$ingredients' },
{ $group: { _id: '$ingredients' } }
]);
// result
[
{ _id: 'tomatos' },
{ _id: 'spaghetti' },
{ _id: 'oil' },
{ _id: 'pepper' },
{ _id: 'salt' },
{ _id: 'pesto' }
]
And finally, I have my list of ingredients. Work for my server: 0, since it's all done by Mongo.
$unwind with $group to count occurences
As a final example, i could also use a combination of $unwind
and $group
to find out in how many recipes each ingredient is used:
Recipe.aggregate([
{ $project: { _id: 0, ingredients: '$ingredients.name' } },
{ $unwind: '$ingredients' },
{ $group: { _id: '$ingredients', occurences: { $sum: 1 } } }
]);
// result
[
{ _id: 'tomatos', occurences: 3 },
{ _id: 'spaghetti', occurences: 1 },
{ _id: 'oil', occurences: 1 },
{ _id: 'salt', occurences: 2 },
{ _id: 'pepper', occurences: 2 },
{ _id: 'pesto', occurences: 1 }
]
While this might seem like a contrived example, you can also think of a Users collection, where each user has a list of favourite books. Then you'd use the above query to find out which book is the most popular.
With $match
, $group
, $project
and $unwind
, I've now covered the most relevant Mongo aggregation stages (with $group
arguably being the most difficult to get your head around). For completion, I'll add one more post to include some relatively simple stages, you can already tell by the name what they're doing:
$count
$limit
$sort
$out
✏ Recap
This post covered:
MongoDB aggregation:
$unwind
stage$unwind
in combination with$group
✏ Thanks for reading!
I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subscribe to my newsletter. Until next time 👋
✏ Previous Posts
You can find an overview of all previous posts with tags and tag search here: