100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
MongoDB

$lookup and Joins in MongoDB

Understand how $lookup performs left outer joins across collections in the aggregation pipeline, from simple equality joins to correlated subqueries.

AggregationAdvanced11 min readJul 10, 2026
Analogies

Why MongoDB Needs $lookup

MongoDB is a document database and typically favors embedding related data within a single document to avoid joins, but embedding isn't always practical: data that grows unbounded, is shared across many parent documents, or is updated independently is often better modeled as a separate collection with a reference. $lookup fills the gap this creates by performing a left outer join, matching documents from a 'foreign' collection into the current pipeline based on a field comparison, and it returns an array of matches on the joined field even when zero, one, or many documents match.

🏏

Cricket analogy: A player profile document referencing a separate 'matches' collection instead of embedding thousands of career innings mirrors why unbounded, growing data is modeled as a separate collection needing $lookup to join back in.

Simple Equality $lookup

The basic form of $lookup takes four fields: 'from' names the foreign collection, 'localField' names the field in the current pipeline's documents, 'foreignField' names the matching field in the foreign collection, and 'as' names the new array field where matched documents are placed. For every input document, MongoDB scans the foreign collection for documents where foreignField equals the input's localField value and attaches all matches as an array; if no documents match, the array is simply empty rather than causing the document to be dropped, which is the defining left-outer-join behavior.

🏏

Cricket analogy: Joining a batter's document to a separate innings collection where innings.playerId equals the batter's _id retrieves every innings they've played, attaching an array even if some batters have zero recorded innings.

javascript
db.orders.aggregate([
  { $lookup: {
      from: "customers",
      localField: "customerId",
      foreignField: "_id",
      as: "customerInfo"
  }},
  { $unwind: "$customerInfo" },
  { $project: { orderId: 1, amount: 1, "customerInfo.name": 1, "customerInfo.email": 1 } }
]);

$lookup followed by $unwind is a very common pairing when you expect exactly one match per input document, effectively simulating an inner join by turning the single-element array into a plain embedded object.

Correlated Sub-Pipeline $lookup

For joins that need more than simple equality — filtering the foreign collection, joining on multiple fields, or running the foreign side through its own aggregation stages — $lookup supports a 'let' clause to define variables from the input document and a nested 'pipeline' that can reference those variables via $expr and $$variableName. This form is strictly more powerful than the simple form and effectively lets you run a correlated subquery per input document, for example joining only the foreign documents that also satisfy a date range or status condition, something the simple localField/foreignField form cannot express.

🏏

Cricket analogy: Joining a bowler to only their wickets taken against a specific opponent within a date range requires a filtered sub-pipeline $lookup, since simple equality alone can't express multiple conditions.

javascript
db.customers.aggregate([
  { $lookup: {
      from: "orders",
      let: { custId: "$_id" },
      pipeline: [
        { $match: { $expr: {
            $and: [
              { $eq: ["$customerId", "$$custId"] },
              { $gte: ["$orderDate", ISODate("2026-01-01")] }
            ]
        }}}
      ],
      as: "recentOrders"
  }}
]);

$lookup performs one lookup operation per input document by default, so an unfiltered $lookup placed early in a pipeline against a large foreign collection can be very slow. Always place $match before $lookup to shrink the input side, and ensure foreignField is indexed on the joined collection.

  • $lookup performs a left outer join, attaching matched foreign documents as an array even when there are zero matches.
  • The simple form uses from, localField, foreignField, and as for straightforward equality joins.
  • $lookup followed by $unwind approximates an inner join when exactly one match is expected per document.
  • The let/pipeline form supports correlated sub-pipelines, enabling filters, multi-field joins, and further aggregation on the foreign side.
  • $expr with $$variableName is required inside a sub-pipeline $lookup to reference variables defined in let.
  • Indexing foreignField on the joined collection is essential for $lookup performance.
  • Filtering with $match before $lookup reduces how many join operations the server must perform.

Practice what you learned

Was this page helpful?

Topics covered

#MongoDB#MongoDBStudyNotes#Database#LookupAndJoinsInMongoDB#Lookup#Joins#Needs#Simple#SQL#StudyNotes#SkillVeris