querydrill

Learn ›Advanced aggregation ›Joining collections with $lookup

What $lookup costs

Syntax is the easy half. The question that separates answers is what $lookup costs - it runs once per input document, which is why filtering before the join matters so much, and why embedding is often the better design.

Be able to say:

$lookup can become expensive, especially with large collections or poor join-field indexing.

If you’re doing:

orders.userId
→
users._id

users._id is already indexed.

But for:

orders.userId
→
users.externalUserId

you probably want an index on:

{ externalUserId: 1 }

Also:

Try it

Filtered down to five documents before the join:

db.orders.aggregate([
  { $match: { status: "completed" } },
  { $limit: 5 },
  {
    $lookup: {
      from: "users",
      localField: "userId",
      foreignField: "_id",
      as: "user"
    }
  },
  {
    $project: {
      status: 1,
      "user.name": 1
    }
  }
])

Move { $limit: 5 } to the end and the answer is identical - but the join ran against every completed order to produce the same five rows. Browser mode has no explain(), so you cannot see the cost here; the point is that the stage order, not the output, is what changed.