MongoDB’s aggregation framework is a versatile tool for transforming and analyzing data. Combining $unwind with other aggregation stages allow you to perform even more complex queries and gain deeper insights from your data.
Understanding $unwind
Link to heading
The $unwind stage in MongoDB’s aggregation pipeline deconstructs an array field from the input documents to output a document for each element of the array. This effectively “flattens” the array, creating multiple documents from a single document that contains an array.
{
$unwind: {
path: <arrayFieldPath>,
includeArrayIndex: <string>, // Optional
preserveNullAndEmptyArrays: <boolean> // Optional
}
}
How $unwind Works
Link to heading
Consider the following collection users:
[
{ "_id": 1, "name": "Alice", "hobbies": ["reading", "cycling", "swimming"] },
{ "_id": 2, "name": "Bob", "hobbies": ["painting", "drawing"] },
{ "_id": 3, "name": "Charlie", "hobbies": [] },
{ "_id": 4, "name": "David" }
]
Applying the $unwind stage on the hobbies field:
{ $unwind: “$hobbies” }
[
{ "_id": 1, "name": "Alice", "hobbies": "reading" },
{ "_id": 1, "name": "Alice", "hobbies": "cycling" },
{ "_id": 1, "name": "Alice", "hobbies": "swimming" },
{ "_id": 2, "name": "Bob", "hobbies": "painting" },
{ "_id": 2, "name": "Bob", "hobbies": "drawing" }
]
To retain documents with empty or non-existent arrays, you can use the preserveNullAndEmptyArrays option:
{ $unwind: { path: "$hobbies", preserveNullAndEmptyArrays: true } }
[
{ "_id": 1, "name": "Alice", "hobbies": "reading" },
{ "_id": 1, "name": "Alice", "hobbies": "cycling" },
{ "_id": 1, "name": "Alice", "hobbies": "swimming" },
{ "_id": 2, "name": "Bob", "hobbies": "painting" },
{ "_id": 2, "name": "Bob", "hobbies": "drawing" },
{ "_id": 3, "name": "Charlie", "hobbies": null },
{ "_id": 4, "name": "David", "hobbies": null }
]
Practical Use Cases for $unwind
Link to heading
1. E-Commerce Order Processing Link to heading
Consider an e-commerce database where each order document contains an array of items purchased:
{
"_id": 101,
"customer": "John Doe",
"items": [
{ "product": "Laptop", "quantity": 1, "price": 1200 },
{ "product": "Mouse", "quantity": 2, "price": 40 }
]
}
Using $unwind, we can flatten the items array to analyze sales data more effectively:
{ $unwind: "$items" }
Result:
[
{ "_id": 101, "customer": "John Doe", "items": { "product": "Laptop", "quantity": 1, "price": 1200 } },
{ "_id": 101, "customer": "John Doe", "items": { "product": "Mouse", "quantity": 2, "price": 40 } }
]
2. Social Media Analytics Link to heading
In a social media platform, each user document might contain an array of posts. To generate reports on individual posts, $unwind can be used to flatten the posts array:
{
"_id": 202,
"username": "janedoe",
"posts": [
{ "post_id": 1, "content": "Hello World!" },
{ "post_id": 2, "content": "MongoDB is awesome!" }
]
}
Using $unwind on the posts array:
{ $unwind: "$posts" }
Result:
[
{ "_id": 202, "username": "janedoe", "posts": { "post_id": 1, "content": "Hello World!" } },
{ "_id": 202, "username": "janedoe", "posts": { "post_id": 2, "content": "MongoDB is awesome!" } }
]
Combining $unwind with $match, $group, and $project
Link to heading
To demonstrate the combined power of these stages, let’s work with an example collection called orders. This collection stores customer orders, each containing an array of items.
Sample Data Link to heading
[
{
"_id": 1,
"customer": "John Doe",
"orderDate": "2023-05-01",
"items": [
{ "product": "Laptop", "quantity": 1, "price": 1200 },
{ "product": "Mouse", "quantity": 2, "price": 25 }
]
},
{
"_id": 2,
"customer": "Jane Smith",
"orderDate": "2023-05-02",
"items": [
{ "product": "Keyboard", "quantity": 1, "price": 100 },
{ "product": "Monitor", "quantity": 1, "price": 300 },
{ "product": "Mouse", "quantity": 1, "price": 25 }
]
}
]
Step-by-Step Aggregation Link to heading
1. $unwind
Link to heading
First, we use $unwind to deconstruct the items array in each order document. This will create separate documents for each item in an order.
{ $unwind: "$items" }
Output:
[
{ "_id": 1, "customer": "John Doe", "orderDate": "2023-05-01", "items": { "product": "Laptop", "quantity": 1, "price": 1200 } },
{ "_id": 1, "customer": "John Doe", "orderDate": "2023-05-01", "items": { "product": "Mouse", "quantity": 2, "price": 25 } },
{ "_id": 2, "customer": "Jane Smith", "orderDate": "2023-05-02", "items": { "product": "Keyboard", "quantity": 1, "price": 100 } },
{ "_id": 2, "customer": "Jane Smith", "orderDate": "2023-05-02", "items": { "product": "Monitor", "quantity": 1, "price": 300 } },
{ "_id": 2, "customer": "Jane Smith", "orderDate": "2023-05-02", "items": { "product": "Mouse", "quantity": 1, "price": 25 } }
]
2. $match
Link to heading
Next, we use $match to filter documents. For example, let’s find all orders containing the product “Mouse”.
{ $match: { "items.product": "Mouse" } }
Output:
[
{ "_id": 1, "customer": "John Doe", "orderDate": "2023-05-01", "items": { "product": "Mouse", "quantity": 2, "price": 25 } },
{ "_id": 2, "customer": "Jane Smith", "orderDate": "2023-05-02", "items": { "product": "Mouse", "quantity": 1, "price": 25 } }
]
3. $group
Link to heading
To aggregate data, we can use $group. Let’s sum up the total quantity and revenue for each product.
{
$group: {
_id: "$items.product",
totalQuantity: { $sum: "$items.quantity" },
totalRevenue: { $sum: { $multiply: ["$items.quantity", "$items.price"] } }
}
}
Output:
[
{ "_id": "Mouse", "totalQuantity": 3, "totalRevenue": 75 },
{ "_id": "Keyboard", "totalQuantity": 1, "totalRevenue": 100 },
{ "_id": "Monitor", "totalQuantity": 1, "totalRevenue": 300 },
{ "_id": "Laptop", "totalQuantity": 1, "totalRevenue": 1200 }
]
4. $project
Link to heading
Finally, we use $project to format the output. Let’s create a more readable output that renames fields and calculates the average price per unit.
{
$project: {
_id: 0,
product: "$_id",
totalQuantity: 1,
totalRevenue: 1,
avgPrice: { $divide: ["$totalRevenue", "$totalQuantity"] }
}
}
Output:
[
{ "product": "Mouse", "totalQuantity": 3, "totalRevenue": 75, "avgPrice": 25 },
{ "product": "Keyboard", "totalQuantity": 1, "totalRevenue": 100, "avgPrice": 100 },
{ "product": "Monitor", "totalQuantity": 1, "totalRevenue": 300, "avgPrice": 300 },
{ "product": "Laptop", "totalQuantity": 1, "totalRevenue": 1200, "avgPrice": 1200 }
]
Full Aggregation Pipeline Link to heading
Combining all these stages, the complete aggregation pipeline looks like this:
[
{ $unwind: "$items" },
{ $match: { "items.product": "Mouse" } },
{
$group: {
_id: "$items.product",
totalQuantity: { $sum: "$items.quantity" },
totalRevenue: { $sum: { $multiply: ["$items.quantity", "$items.price"] } }
}
},
{
$project: {
_id: 0,
product: "$_id",
totalQuantity: 1,
totalRevenue: 1,
avgPrice: { $divide: ["$totalRevenue", "$totalQuantity"] }
}
}
]
Conclusion Link to heading
Using $unwind in conjunction with other aggregation stages like $match, $group, and $project allows you to perform sophisticated data transformations and analysis in MongoDB. Whether you are analyzing e-commerce data, social media interactions, or any other type of array-based data, these techniques can help you unlock deeper insights and create more meaningful reports.