Advanced Aggregation
$lookup + $unwind
Flatten a single matched document out of the lookup array — the pattern after most joins.
After a 1:1 join the array has zero or one element, and you still cannot write "user.name" the way you would on an object. $unwind turns that array into a field you can project. This is the pattern after most $lookups that resolve an id to a document.
db.orders.aggregate([
{ $match: { status: { $ne: "cancelled" } } },
{ $group: { _id: "$userId", totalSpent: { $sum: "$amount" } } },
{ $sort: { totalSpent: -1 } },
{ $limit: 3 },
{ $lookup: { from: "users", localField: "_id", foreignField: "_id", as: "user" } },
{ $unwind: "$user" },
{ $project: { _id: 0, name: "$user.name", city: "$user.city", totalSpent: 1 } }
]){ name: "Rahul", city: "Bangalore", totalSpent: 18400 }
{ name: "Priya", city: "Pune", totalSpent: 15250 }$group → $sort → $limit → $lookup. Three users resolved, not every userId in orders. $unwind without preserveNullAndEmptyArrays drops parents whose array is empty — a left join becomes an inner join. That is correct here (an order group must have a user). It is wrong when missing users should still appear.
Interview question
Why $unwind after $lookup, and when does that drop documents?
The lookup result is always an array. Unwind turns a one-element array into an object so I can project user.name. Without preserveNullAndEmptyArrays, an empty array removes the parent — left join becomes inner join.
Practice
Group and cut first. Join three rows.