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:
// ❌ 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
Norders (N = 100). - 100 separate queries fetch the user for each individual order.
- Total:
100 + 1 = 101database 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 = 8ms101 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)
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:
// ✅ 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:
SELECT * FROM orders LIMIT 100;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?
Interactive Roadmap & Tool
Read next
- CZENCutting Through the Noise: Which Backend Language and Framework Should You Choose in 2026?
Cutting Through the Noise: Which Backend Language and Framework Should You Choose in 2026?
October 25, 2026 · 4 min read - CZENInside PHP CVE-2026-115560: Session Upload Progress & Serialize Handler Mismatch Explained
Inside PHP CVE-2026-115560: Session Upload Progress & Serialize Handler Mismatch Explained
October 24, 2026 · 3 min read