Skip to content

Advanced Aggregation

$lookup + $unwind

Flatten a single matched document out of the lookup array — the pattern after most joins.

IntermediateAbout 6 minutes

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 } }
])
Top spenders — join three documents, not every order
{ 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?

Think about it first.

Practice

Group and cut first. Join three rows.