#100DaysOfMERN - Day 51

#100DaysOfMERN - Day 51

·

4 min read

✏ 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:

aggregation-example-collection.png

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 $projectstage, 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:

#100DaysOfMERN - The App