Advanced Aggregation
$lookup
A left outer join between collections. Local field, foreign field, and the array you get back.
$lookup is a left outer join. Every input document stays. Matching documents from another collection land in an array on the field you name. No match → that array is [], not a missing document.
db.users.aggregate([
{ $match: { name: "Rahul" } },
{
$lookup: {
from: "orders",
localField: "_id",
foreignField: "userId",
as: "orders"
}
}
]){ _id: ObjectId("64a1b2c3d4e5f6a7b8c90001"), name: "Rahul", city: "Bangalore",
orders: [
{ userId: ObjectId("64a1b2c3d4e5f6a7b8c90001"), amount: 4900, status: "completed", ... },
{ userId: ObjectId("64a1b2c3d4e5f6a7b8c90001"), amount: 1200, status: "pending", ... }
]
}| Field | Meaning |
|---|---|
| from | Collection to join |
| localField | Field on the current document |
| foreignField | Field on documents in from |
| as | New array field. Overwrites if the name already exists |
Equality only — and types must match. ObjectId on _id will not join a userId stored as a string. $size on the array is a count, including 0 for users with no orders. $match before $lookup so you join Pune, not the collection. An index on orders.userId (Indexing module) is what stops this from scanning orders once per user.
Interview question
What does $lookup do, and what should you watch out for?
A left outer join: matching documents from another collection become an array field. No match is []. It runs per remaining input document, so $match / $limit first. Foreign field should be indexed. The joined array counts toward the 16MB document limit. A join on every read is often a modelling smell.
Practice
Start from users, join orders, count the array.