mongodb 7.5.0MongoDB $lookup Examples for Relationships
Use practical MongoDB $lookup examples to join related collections, flatten arrays, filter joined data, inspect plans, and decide when embedding is better.
Start with the ownership decision
MongoDB supports both embedded and referenced relationships. Embed data when it belongs to one parent, is normally read with that parent, and remains bounded. Reference another document when it has an independent lifecycle, is shared by many records, or grows separately.
$lookup can combine referenced data for a read. It is a left outer join: every input document continues through the pipeline, and matching foreign documents are placed in an array. The stage is useful, but it should not replace a deliberate data model. Excessive joins can add latency and make indexing more important.
Prepare two related collections
This dataset stores authors independently and references their _id values from books:
use("mongog_tutorial");
const authors = db.collection("lookup_authors");
const books = db.collection("lookup_books");
await books.deleteMany({ tutorial: "lookup" });
await authors.deleteMany({ tutorial: "lookup" });
const adaId = ObjectId();
const graceId = ObjectId();
await authors.insertMany([
{ _id: adaId, name: "Ada", region: "EU", tutorial: "lookup" },
{ _id: graceId, name: "Grace", region: "US", tutorial: "lookup" },
]);
await books.insertMany([
{ title: "Analytical Engines", authorId: adaId, rating: 4.8, tutorial: "lookup" },
{ title: "Compiler Patterns", authorId: graceId, rating: 4.9, tutorial: "lookup" },
{ title: "Unassigned Notes", authorId: ObjectId(), rating: 3.5, tutorial: "lookup" },
]);
Both sides use actual ObjectId values. A hexadecimal string that merely looks like an ObjectId would not match the BSON type.
Perform an equality lookup
Join lookup_books.authorId to lookup_authors._id:
books.aggregate([
{ $match: { tutorial: "lookup" } },
{
$lookup: {
from: "lookup_authors",
localField: "authorId",
foreignField: "_id",
as: "author",
},
},
{
$project: {
_id: 0,
title: 1,
rating: 1,
author: { name: 1, region: 1 },
},
},
{ $sort: { title: 1 } },
]);
Expected result
Each result contains an author array. The two matching books contain one author document. Unassigned Notes remains in the result with an empty array because $lookup preserves the left-side book.
The from collection is in the same database. If related records live in another service or deployment, combine them in the application rather than treating $lookup as a distributed join mechanism.
Flatten a one-to-one result
When the relationship is expected to produce at most one author, $unwind can turn the one-element array into an embedded document:
books.aggregate([
{ $match: { tutorial: "lookup" } },
{
$lookup: {
from: "lookup_authors",
localField: "authorId",
foreignField: "_id",
as: "author",
},
},
{
$unwind: {
path: "$author",
preserveNullAndEmptyArrays: true,
},
},
{
$project: {
_id: 0,
title: 1,
authorName: "$author.name",
authorRegion: "$author.region",
},
},
]);
With preserveNullAndEmptyArrays: true, unmatched books remain visible. Remove that option only when an inner-join-like result is intentional.
Filter the foreign pipeline
The pipeline form supports additional foreign-side conditions and projection. This example returns only highly rated books and only EU author details:
books.aggregate([
{ $match: { tutorial: "lookup", rating: { $gte: 4.5 } } },
{
$lookup: {
from: "lookup_authors",
localField: "authorId",
foreignField: "_id",
pipeline: [
{ $match: { region: "EU", tutorial: "lookup" } },
{ $project: { _id: 0, name: 1, region: 1 } },
],
as: "euAuthor",
},
},
{ $match: { "euAuthor.0": { $exists: true } } },
]);
Filter early on the local side so fewer documents enter the join. On the foreign side, equality against _id already uses its automatic index. For other join keys, create an index that supports foreignField and any selective pipeline predicates.
Decide whether to embed instead
If every book page always needs the author’s display name, storing a small author snapshot in the book may remove a join from the main read path. The authoritative author can remain referenced while the snapshot is updated through an explicit synchronization rule.
Duplication is not automatically a flaw; unowned duplication is. Document which copy is authoritative, when snapshots may be stale, and how writers update them. Conversely, do not embed a growing list of every book inside an author when that array can become unbounded.
Explain the pipeline
Use an explain plan with representative data before relying on a join-heavy path:
books.explain("executionStats").aggregate([
{ $match: { tutorial: "lookup", rating: { $gte: 4.5 } } },
{
$lookup: {
from: "lookup_authors",
localField: "authorId",
foreignField: "_id",
as: "author",
},
},
]);
Check the local $match, indexes on foreign join fields, documents examined, and result cardinality. A small tutorial collection cannot predict production performance.
Practice exercise
Add a lookup_publishers collection and a publisherId reference on two books. Build a pipeline that:
- Keeps books rated at least 4.5.
- Joins both the author and publisher.
- Preserves books with no publisher.
- Projects a flat title, author name, and publisher name.
- Sorts deterministically by rating and title.
Then identify which foreign fields need indexes and whether either relationship would be simpler as an embedded snapshot.
Common mistakes
- Comparing a string identifier with a stored
ObjectIdreference. - Assuming
$lookupremoves unmatched local documents; it creates an empty result array. - Forgetting that
$unwindcan remove empty arrays unless preservation is requested. - Joining large collections without an index on the foreign match field.
- Using repeated
$lookupstages to reproduce a normalized relational schema without reconsidering embedding. - Treating duplicated snapshots as automatically consistent without an ownership rule.
Continue learning
Review data modeling before adding more joins, combine related records with aggregation pipeline examples, and summarize them with $group examples. MongoDB documents the full $lookup stage and relationship modeling patterns.