awesome-everything RU
↑ Back to the climb

Performance

N+1: code and log reading

Crux Read real ORM loops, a DataLoader batch function, a query log, and a fan-out handler — predict the round-trip count and pick the highest-leverage fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min

N+1 is diagnosed by reading code and logs, not by guessing. Read the snippet or the trace, count the round-trips it will fire, then choose the fix a senior engineer reaches for first.

Goal

Practise the loop you run in every N+1 incident: read the hot path, count the round-trips, and reach for the structural fix that matches the data shape — before touching indexes or hardware.

Snippet 1 — the innocent loop

const orders = await Order.findAll({ where: { userId } }); // 50 rows
for (const order of orders) {
  const customer = await order.getCustomer(); // one query per order
  render(order, customer.name);
}
Quiz

With 50 orders, how many queries does this fire, and what is the single highest-leverage fix?

Snippet 2 — the DataLoader batch function

const userLoader = new DataLoader(async (ids) => {
  const users = await db.user.findMany({ where: { id: { in: ids } } });
  return users; // returned in DB order
});
// resolver: return userLoader.load(post.authorId)
Quiz

This DataLoader batches correctly but has a correctness bug a load test will expose. What is it?

Snippet 3 — the query log

Started GET "/dashboard"
User Load (0.4ms)     SELECT * FROM users WHERE id = 42
Project Load (0.6ms)  SELECT * FROM projects WHERE user_id = 42 LIMIT 50
Task Load (0.3ms)     SELECT * FROM tasks WHERE project_id = 1
Task Load (0.3ms)     SELECT * FROM tasks WHERE project_id = 2
... (48 more Task Load lines)
Comment Load (0.2ms)  SELECT * FROM comments WHERE task_id = 1
... (243 more Comment Load lines)
Completed 200 OK in 980ms (Views: 250ms | ActiveRecord: 689ms)
Quiz

Reading this log, what is happening and what is the precise fix?

Snippet 4 — the fan-out handler

const user   = await userService.get(id);    // 30 ms
const posts  = await postsService.get(id);    // 30 ms
const notifs = await notifService.get(id);    // 30 ms
const billing = await billingService.get(id); // 30 ms
return assemble(user, posts, notifs, billing);
Quiz

These four service calls are independent (none consumes another's result). What is the wall-clock now, and the fix?

Recap

Every N+1 is read in code or logs: a lazy relation inside a loop is 1+N queries fixed by eager loading; a DataLoader batch function must return results positionally aligned to its input ids; a query log with repeated similar SELECTs reveals nested N+1 levels fixed by nested preload; and independent serial service calls collapse from sum to max via parallel dispatch. Count the round-trips first, match the fix to the data shape, then re-read the log to confirm the count dropped.

Continue the climb ↑N+1: diagnose, batch, and gate
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.