Writing

The Database Killer: What is the N+1 Query Problem and How to Fix It with Eager Loading

Code that runs in 5ms on your laptop with 3 test rows can easily bring down your production PostgreSQL server when listing 100 orders. Here is how the N+1 query trap happens in modern ORMs and how to fix it in one line.

Furkan ÖzayOctober 20, 2026 · 3 min read
czCZAY.DEV // EDITORIALEN
DATABASE INTERNALS

#Veritabanı · 3 MIN READ

FURKAN ÖZAYVOL. 2026 // #2C8A

Every backend engineer has experienced this mystery at least once: You build an API endpoint to list recent orders along with each customer's name. On your local development machine with 4 seed records, the endpoint responds in a blazing 6 milliseconds.

You deploy to production. As soon as traffic hits and the page renders 100 orders per request, CPU usage on your PostgreSQL server spikes to 100%, connection pools exhaust, and response times crawl to 3.5 seconds.

The culprit? The most notorious performance anti-pattern in ORM history: The N+1 Query Problem.

· · ·

1. Anatomy of an N+1 Query

Look at this seemingly innocent code that fetches the latest 100 orders and attaches each customer's details:

typescript
// ❌ THE N+1 TRAP: 1 query for orders + 100 separate queries inside the loop!
const orders = await db.query('SELECT * FROM orders LIMIT 100'); // -> 1st Query (1)
 
for (const order of orders) {
  // Executes a brand-new network roundtrip to PostgreSQL on EVERY iteration!
  order.user = await db.query('SELECT * FROM users WHERE id = $1', [order.user_id]); // -> 100 Queries (N)
}

Let's do the math:

  • 1 query fetches the list of N orders (N = 100).
  • 100 separate queries fetch the user for each individual order.
  • Total: 100 + 1 = 101 database queries executed for a single HTTP request!

Why Network Roundtrip Latency Kills You

Even if PostgreSQL executes each individual SELECT * FROM users WHERE id = $1 in 0.2ms using a primary key index, every separate query requires a TCP network roundtrip between your API container and your database server (~3ms to 5ms in cloud VPCs).

  • 2 queries × 4ms network overhead = 8ms
  • 101 queries × 4ms network overhead = 404ms (for just one user refreshing the page!)
· · ·

2. The Solution: Eager Loading (JOIN or WHERE IN)

Instead of asking the database 100 separate questions one by one, Eager Loading fetches all required data in 1 or 2 queries total.

Approach A: SQL LEFT JOIN (1 Query Total)

SQL
SELECT orders.*, users.name, users.email
FROM orders
LEFT JOIN users ON orders.user_id = users.id
LIMIT 100;

Approach B: Batch WHERE id IN (...) (2 Queries Total)

Modern ORMs like Prisma, Drizzle, Eloquent (Laravel), and Django let you enable eager loading with a single property:

typescript
// ✅ PRISMA: Uses `include` to batch-fetch all related users in 1 extra query (WHERE id IN (...))
const orders = await prisma.order.findMany({
  take: 100,
  include: {
    user: true, // <- Eliminates the N+1 loop!
  },
});
 
// ✅ DRIZZLE ORM: Relational query with `with`
const orders = await db.query.orders.findMany({
  limit: 100,
  with: {
    user: true,
  },
});

Under the hood, the ORM runs:

  1. SELECT * FROM orders LIMIT 100;
  2. SELECT * FROM users WHERE id IN (12, 15, 19, 44, ...);

And stitches the objects together in memory in microseconds! 101 queries drop to 2 queries.

· · ·

Interactive Diagnostic Tool

Want to test your own ORM patterns and walk through our step-by-step N+1 detection checklist?

👉 Launch the Interactive N+1 Query Diagnostic Tool

Interactive Roadmap & Tool

Read next