← All posts

The N+1 query problem, and why your ORM hides it

One query for the list, then one more per row, forever

Key takeaways
  • Loop bodies that touch the database are the smell.
  • Log your SQL in development — the count is the tell.
  • Fix it by fetching related rows in one query, not by caching the symptom.

You fetch 50 posts. Then, for each one, you read its author. That is 1 + 50 = 51 queries where 2 would do.

const posts = await postRepo.find();          // 1 query

for (const post of posts) {
  post.author = await userRepo.findOne(post.authorId);  // 50 more
}

It looks harmless because each individual query is fast. The problem is the round trips. At 5ms each, 50 queries is 250ms of pure latency — and it grows linearly with your data.

Why ORMs make it easy to miss

Lazy loading means post.author looks like a property access. There is no visible await, no obvious query. The code reads like it is working with objects in memory. It is not.

Finding it

Turn on SQL logging in development and watch the count for a single page load. If loading 50 rows fires 51 statements, you have it. The number scaling with your row count is the confirmation.

Fixing it

Fetch the related rows in one go — a join, or a second query batched by id:

const posts = await postRepo.find({ relations: ['author'] });

Two queries regardless of row count.

The trap

The tempting fix is caching the author lookup. That hides the symptom in development, where your dataset is small and warm, and leaves it fully intact in production where it is not.