Mongoose populate: how to handle document references
populate method, ObjectId ref, virtual populate, nested populate, select in populate, lean query optimization, N+1 problem awareness
Setting up document references
When a document stores an ObjectId reference to another collection, Mongoose's populate() resolves that reference by running a second query to fetch the referenced documents and replacing the raw IDs with the full objects. It is syntactic sugar over a manual findById call.
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
body: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }]
})
// populate triggers a second DB query — one per referenced field
const post = await Post.findById(id)
.populate('author', 'name email -_id') // select fields to include
.populate({ path: 'comments', select: 'body createdAt' })
// Nested populate — resolve author of each comment
await Post.findById(id).populate({
path: 'comments',
populate: { path: 'author', select: 'name' }
})The N+1 problem and how to fix it
Fetching a list of 100 posts and calling .populate('author') runs 1 query for posts plus up to 100 additional author lookups — 101 total. For list endpoints serving high traffic, use $lookup in an aggregation pipeline instead to resolve references in one database roundtrip. Add .lean() for plain JavaScript objects with significantly lower memory overhead when you only need to read data.
// lean() — returns plain JS objects, up to 10x faster
const posts = await Post.find().lean()