Embedding keeps related data in one document, but referenced data — customers, products, authors — has to be joined at read time. $lookup performs that join inside the aggregation pipeline, and $unwind turns array elements into separate documents so they can be grouped, filtered or joined individually. After this lesson you will be able to write equality and correlated $lookup stages, unwind arrays without losing documents, and know when a join is the wrong design.
The basic form matches a local field against a field of another collection in the same database and stores every match in a new array field:
db.orders.aggregate([
{ $lookup: {
from: "customers", // collection to join
localField: "customerId", // field in orders
foreignField: "_id", // field in customers
as: "customer" // output array
} }
])
// { _id: 1, customerId: 42, customer: [ { _id: 42, name: "Ada", ... } ] }as is always an array, even for a one-to-one relation, and it is empty when nothing matches. If localField is an array, each element is matched, which makes $lookup work for tags: ["a", "b"] against a tags collection. The types must match exactly: a string customerId never joins an ObjectId _id.
$unwind emits one document per array element, copying the other fields:
{ $unwind: "$items" }
// Long form
{ $unwind: { path: "$items", includeArrayIndex: "pos", preserveNullAndEmptyArrays: true } }By default documents whose array is missing, null or empty are dropped. preserveNullAndEmptyArrays: true keeps them with the field absent, which matters when an order without items must still appear in a report. includeArrayIndex records the original position.
$unwind followed by $group is the classic way to aggregate array contents:
db.orders.aggregate([
{ $unwind: "$items" },
{ $group: { _id: "$items.productId", units: { $sum: "$items.qty" } } },
{ $sort: { units: -1 } },
{ $limit: 5 }
])Many tutorials follow $lookup with $unwind to turn the one-element array into an object. That silently drops documents with no match. For one-to-one references prefer an expression:
{ $set: { customer: { $first: "$customer" } } } // null when unmatched, document keptUse $unwind on the joined array only when several matches are expected and each should become its own document.
The pipeline form runs a sub-pipeline per input document. Variables declared in let are referenced as $$name inside $expr, and the sub-pipeline can filter, project, sort and limit:
db.customers.aggregate([
{ $lookup: {
from: "orders",
let: { cid: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$customerId", "$$cid"] }, status: "paid" } },
{ $sort: { placedAt: -1 } },
{ $limit: 3 },
{ $project: { total: 1, placedAt: 1 } }
],
as: "recentOrders"
} }
])Since MongoDB 5.0 you can combine both forms — localField/foreignField for the equality plus a pipeline for extra conditions — which lets the planner use an index on the foreign field while still filtering.
$lookup is a nested loop: for each input document it queries from. Index foreignField (the _id index already covers joins on _id).$match and $project before $lookup, not after.$limit inside the pipeline.$graphLookup handles recursive relations such as org charts and threaded comments; $unionWith appends the documents of another collection rather than joining them.localField and foreignField produces empty arrays with no error.$unwind after $lookup on optional relations, dropping every document without a match.$lookup across databases. Both collections must be in the same database.After `$lookup` with `as: "customer"`, what is the type of `customer` when exactly one document matches?
$lookup joins collections in the same database; the result field is always an array and requires matching types.foreignField and shrink the input with $match and $project before joining.$unwind creates one document per array element and drops empty or missing arrays unless preserveNullAndEmptyArrays is set.$first rather than $unwind, so unmatched documents survive.let/pipeline form supports correlated sub-queries with filtering, sorting and limits; $graphLookup covers recursive relations.Next lesson: Multi-Faceted Aggregation: $facet, $bucket, $out and $merge — compute several summaries in one pass, group values into ranges, and write pipeline results to collections.