MongoDB $lookup – Join Collections with Examples, Pipelines, and $unwind | DbSchema



  1. Introduction to MongoDB
  2. Installation & Database Creation
  3. CRUD Operations
  4. Embedded Documents and Arrays
  5. Validation Rules - Enforcing Structure in MongoDB
  6. Visualize MongoDB Relationships (Embedded vs Referenced)
  7. What Is an Index in MongoDB?
  8. Aggregation Pipeline Explained.
  9. $lookup Explained - How to Join Collections (You are here).

Table of Contents

  1. What $lookup is
  2. Basic structure
  3. When to use $lookup vs embedding
  4. Join examples with $unwind and pipelines
  5. Performance tips
  6. Build $lookup pipelines in DbSchema
  7. FAQ
  8. Summary

What Is $lookup?

The $lookup stage in MongoDB lets you join data from another collection. If you’ve worked with SQL, think of it as the MongoDB version of a LEFT OUTER JOIN.

Use it when you need to bring related data together, like combining movies with their reviews or users with their orders.

You use the .aggregate() method in MongoDB and add $lookup as one of the stages.


Basic Structure

Here’s the basic $lookup syntax:

db.collection.aggregate([
  {
    $lookup: {
      from: "<otherCollection>",
      localField: "<field_in_current>",
      foreignField: "<field_in_other>",
      as: "<output_array_field>"
    }
  }
]);
  • from - the collection you want to join

  • localField - the field in the current collection

  • foreignField - the field in the other collection

  • as - the name of the new array field that stores the results

When to use $lookup vs embedding

MongoDB does not force you into joins the way relational databases do, so an important design question is whether you should join collections with $lookup or embed related data in the same document.

Use $lookup when...Use embedding when...
related data changes independentlyrelated data is usually read together
documents would become too large if embeddedthe child data stays small and bounded
you need a reusable shared entity, such as users or productsthe embedded array is part of the parent's natural shape
you want to avoid duplicating frequently changing valuesyou want fewer reads and simpler application code

If you need a broader design guide, read Visualize MongoDB Relationships and MongoDB Schema Design 2026.

Join examples with $unwind and pipelines

Example 1: Join Movies with Reviews

We have two collections:

  • movie

  • reviews

Each review stores the movie_id it belongs to.

db.movie.aggregate([
  {
    $lookup: {
      from: "reviews",
      localField: "_id",
      foreignField: "movie_id",
      as: "movie_reviews"
    }
  }
]);

This query adds a movie_reviews array to each movie document containing all matching reviews.

Lookup Join

Example 2: Flatten the Result with $unwind

By default, $lookup puts the results in an array. If you want one document per review, use $unwind:


db.movie.aggregate([
  {
    $lookup: {
      from: "reviews",
      localField: "_id",
      foreignField: "movie_id",
      as: "movie_reviews"
    }
  },
  { $unwind: "$movie_reviews" }
]);

Now each review appears as its own document linked to the movie.

Lookup $unwind

Why use $unwind after $lookup?

By default, $lookup adds the matching documents as an array field. For example, movie_reviews will contain a list of all reviews for each movie.

Sometimes this is exactly what you want.

Example 3: Join Across Multiple Levels

You can also chain $lookup stages to bring in more collections. For example, join movie → reviews → users (to see who wrote each review):

db.movie.aggregate([
  {
    $lookup: {
      from: "reviews",
      localField: "_id",
      foreignField: "movie_id",
      as: "movie_reviews"
    }
  },
  { $unwind: "$movie_reviews" },
  {
    $lookup: {
      from: "users",
      localField: "movie_reviews.user_id",
      foreignField: "_id",
      as: "review_user"
    }
  },
  { $unwind: "$review_user" }
]);

This produces one document per movie-review-user combination.

Lookup Multiple Join

Example 4: Using a Pipeline in $lookup

Since MongoDB 3.6, $lookup can include a pipeline. This gives you more control, such as filtering the joined documents.

db.movie.aggregate([
  {
    $lookup: {
      from: "reviews",
      let: { movieId: "$_id" },
      pipeline: [
        { $match: { $expr: { $eq: ["$movie_id", "$$movieId"] } } },
        { $project: { user_id: 1, rating: 1, _id: 0 } }
      ],
      as: "filtered_reviews"
    }
  }
]);

Here, only selected fields (user_id, rating) are kept in filtered_reviews.

Pipeline-based MongoDB lookup example

Performance tips

Competitive $lookup guides almost always add performance guidance because joins can get expensive quickly.

  1. Index the join field on the foreign collection whenever possible.
  2. Project only needed fields in pipeline-based lookups.
  3. Filter early with $match before large joins.
  4. Be careful with $unwind because it can multiply result rows.

For example, indexing reviews.movie_id helps this join perform much better:

db.reviews.createIndex({ movie_id: 1 })

If your aggregation starts getting large, run explain() and inspect how much data is scanned before and after the join.

Build $lookup pipelines in DbSchema

DbSchema is useful here because $lookup pipelines are easier to understand when you can see the collections and relationships side by side.

  1. Open MongoDB through the MongoDB tool page or connect with the MongoDB JDBC page.
  2. Inspect your collections visually in the diagram before writing the pipeline.
  3. Build and test the aggregation in the query interface.
  4. Compare the pipeline output with the modeled relationships in your schema diagram.

That workflow is especially helpful when you are translating SQL-style joins into MongoDB or debugging multi-stage pipelines with $lookup, $unwind, and $project.

FAQ

Is $lookup the same as a SQL JOIN?

It is closest to a left outer join. MongoDB adds the matches as an array field on each source document.

What happens if $lookup finds no match?

MongoDB returns an empty array in the as field. If you later use $unwind, you may also need preserveNullAndEmptyArrays.

Can I join on fields other than _id?

Yes. $lookup can join on any matching fields, and pipeline-based lookups can express more advanced conditions.

When should I avoid $lookup?

Avoid it for very large, frequently repeated joins when embedding would be simpler or when the join field is not indexed and performance is critical.

Summary

The $lookup stage lets you combine collections in MongoDB - just like a join in SQL.

You’ve learned how to:

  • Use $lookup for simple joins
  • Flatten results with $unwind
  • Chain multiple $lookup stages
  • Use pipelines inside $lookup for advanced filtering

This is especially useful when visualized in DbSchema, where you can see relationships as diagrams and keep the pipeline logic aligned with the data model.

DbSchema Database Design & Management

Visual Design with ER Diagrams
DbSchema ER Diagram Features Overview
Visual Design & Schema Diagram

✓ Create and manage your database schema visually through a user-friendly graphical interface.

✓ Easily arrange tables, columns, and foreign keys to simplify complex database structures, ensuring clarity and accessibility.

GIT & Collaboration
Version Control & Collaboration

✓ Manage schema changes through version control with built-in Git integration, ensuring every update is tracked and backed up.

✓ Collaborate efficiently with your team to maintain data integrity and streamline your workflow for accurate, consistent results.

Data Explorer & Query Builder
Relational Data & Query Builder

✓ Seamlessly navigate and visually explore your database, inspecting tables and their relationships.

✓ Build complex SQL queries using an intuitive drag-and-drop interface, providing instant results for quick, actionable insights.

Interactive Documentation & Reporting
HTML5 Documentation & Reporting

✓ Generate HTML5 documentation that provides an interactive view of your database schema.

✓ Include comments for columns, use tags for better organization, and create visually reports.