Filter concepts by levelShowing all levels.

MongoDB · $lookup and Join-Like Operations

$lookup and Join-Like Operations

Joining across collections: equality and pipeline forms, their cost, and when to denormalize instead

  • Always a left outer joinunmatched documents survive with an empty array
  • let + pipelinefor joins beyond a single equality condition
  • Index the foreignFieldor the join degrades toward a repeated scan
  • Not always the answerhot-path reads usually want denormalized data instead

1.$lookup for collection joins

$lookup is an aggregation stage that joins documents from another collection into the current one, attaching the matches as a new array field. It is how MongoDB reaches across collections when embedding was not chosen for that relationship.

Key features

  • $lookup runs against a second collection, matching a local field to a foreign field
  • The joined documents are always attached as a new array field, even when only one match exists
  • It always behaves like a LEFT OUTER JOIN: an input document with no matches still survives, with an empty array
  • The foreign collection must be in the same database (or use a sharded-cluster-aware form) — $lookup cannot join across separate deployments
  • A joined array of matches usually needs $unwind right after it, if the pipeline expects one match per input document
$lookup attaches matches as an array, always a left outer join
localFieldforeignFieldattachesas array

orders

local collection

$lookup

match customerId = _id

customers

foreign collection

order + customer[]

array field, even if 0 or 1 match

  • orders — local collection
    • leads to $lookup (localField)
  • $lookup — match customerId = _id
    • leads to order + customer[] (attaches as array)
  • customers — foreign collection
    • leads to $lookup (foreignField)
  • order + customer[] — array field, even if 0 or 1 match

Command

text
{ $lookup: { from, localField, foreignField, as } }

$lookup is a left outer join across collections — every input document survives, matches always land as an array field, and an unmatched document gets an empty array rather than being dropped.

2.Equality joins vs. pipeline $lookup

The localField/foreignField form of $lookup can only express "these two fields are equal". The pipeline form (using let and a nested pipeline) can express any condition — ranges, multiple fields, or additional filtering — at the cost of more to write.

Key features

  • Equality form: { from, localField, foreignField, as } — exactly one equality condition
  • Pipeline form: { from, let, pipeline, as } — let exposes local fields as variables for the nested pipeline to use
  • The nested pipeline can $match on $expr with multiple conditions, ranges, or computed values — not just equality
  • A pipeline-based $lookup can also $project the foreign collection down before joining, reducing how much data is attached
  • Multiple equality lookups can be simulated with a pipeline-form $lookup and an $and of $expr conditions

Command

text
{ $lookup: { from, let, pipeline: [{ $match: { $expr: {...} } }], as } }

Use localField/foreignField for a plain equality join — reach for the pipeline form with let only when the join needs a range, multiple conditions, or filtering the foreign side before it is attached.

3.$lookup performance implications

$lookup does one query against the foreign collection per batch of input documents, so it is only fast when the foreign field is indexed — without an index it is a collection scan run repeatedly, once per batch, instead of once.

Key features

  • The foreignField should be indexed — without one, $lookup effectively runs a collection scan per batch of input documents
  • The server batches multiple local documents into fewer foreign-collection queries where it can, but this optimization depends on the join being equality-based
  • A pipeline-form $lookup with an unindexed condition inside its nested pipeline loses this batching benefit entirely
  • A $lookup that joins in a large fraction of the foreign collection can produce documents far larger than either source document
  • In a sharded cluster, $lookup can require broadcasting to multiple shards for the foreign collection, adding network cost

Command

text
// Always index the foreignField — the field $lookup matches against on the "from" collection

Always index foreignField before shipping a $lookup — without it, the join degrades toward a per-batch collection scan on the foreign collection, and the cost grows with input size.

4.$lookup vs. denormalized design

$lookup is the right tool for occasional, reporting-style joins across data that genuinely belongs in separate collections. For a relationship read on every hot-path request, embedding or duplicating the needed fields is usually faster, because it avoids a join on every read.

Key features

  • $lookup fits infrequent, reporting/analytics-style queries where join cost is acceptable
  • $lookup fits queries where the "many" side changes independently and duplicating it everywhere would mean updating many places on every change
  • Denormalization (embedding or duplicating fields) fits data read together on a hot path, where a join on every request adds real latency
  • A common middle ground: embed only the few fields actually displayed (extended reference pattern), and $lookup the rest only when a user drills in
  • The decision is about the read pattern, not about "which is more correct" — both are valid schema choices for different access patterns

Choosing between $lookup and denormalization

Choosing between $lookup and denormalization
SignalFavors
Read on every hot-path requestdenormalize (embed or duplicate fields)
Occasional report or admin view$lookup
Referenced data changes often and independently$lookup (avoids updating many duplicates)
Only a few display fields are ever neededextended reference pattern — embed those, $lookup the rest on demand

Command

text
// Ask: is this relationship read on a hot path, and does the "many" side change independently?

Denormalize what a hot-path read needs every time; reach for $lookup for occasional, reporting-style joins where the read frequency does not justify duplicating the data everywhere.

5.Join field cardinality

Cardinality — how many foreign documents match one local document — shapes both the index needed and the size of the result. A high-cardinality join (one order matching thousands of log entries) produces large arrays and deserves a $limit or a narrower condition; a low-cardinality join (one order, one customer) does not.

Signature

low cardinality → small joined array · high cardinality → cap with $limit inside the pipeline form

An indexed equality join is fast regardless of cardinality — the risk is downstream: high cardinality produces large joined arrays, which is a document-size and memory concern, not just an index concern.

Remember

  • $lookup output is always an array field, even for a single match — index it or $unwind if a one-to-one join is expected.
  • localField/foreignField only expresses equality — use let + pipeline for anything more.
  • Always index the foreignField before shipping a $lookup.
  • Prefer denormalizing data that is read on every hot-path request; save $lookup for occasional joins.

Pro tip

  • A pipeline-form $lookup can $project the foreign side down before attaching it, cutting how much data joins in.
  • High-cardinality joins need a $limit or a narrower match — the index makes matching fast, not the result small.

Next up

  • Window Functions and Advanced Analytics
  • Transactions
  • Atomicity and Concurrency

Sources

  • $lookup — mongodb.com/docs/manual/reference/operator/aggregation/lookup/
  • Data modeling design patterns — mongodb.com/docs/manual/data-modeling/design-patterns/
  • Checked 2026-09-10

Spotted a mistake on this page? Tell us.