The N+1 Query Problem

One query for a list, then one more for every item — a silent performance killer in ORMs.

Intermediate6 min read3 interview questions

The N+1 problem happens when code loads a list of N records with one query, then runs another query for each record to load related data. Fifty orders become fifty-one database round trips.

It is easy to create with ORMs such as Entity Framework or Hibernate, because lazy-loaded navigation properties look like normal property access.

Diagram

N+1 — 1 + N round tripsFixed — 1 or 2 round tripseager loadAppSELECT orders1 querySELECT customer× N queriesAppSELECT ordersJOIN customers
Loading related data inside a loop multiplies round trips; a join or batched query fixes it.

Key concepts

Lazy loading
Related data is fetched the first time it is accessed — convenient, and the main cause of N+1.
Eager loading
Related data is fetched up front with a join or a second batched query (Include, JOIN FETCH).
Batching
Collect all IDs and run one WHERE id IN (…) query. GraphQL DataLoader does this automatically.
Projection
Select only the fields needed into a DTO, letting the database do the join.

Example

Entity Framework Core: N+1 and the fix
// N+1: one query for orders, then one per order for its customer
var orders = await db.Orders.ToListAsync();
foreach (var o in orders)
    Console.WriteLine(o.Customer.Name);   // lazy load = extra query each time

// Fix: load the related data in the same query
var orders = await db.Orders
    .Include(o => o.Customer)
    .ToListAsync();

// Or project only what you need
var rows = await db.Orders
    .Select(o => new { o.Id, CustomerName = o.Customer.Name })
    .ToListAsync();

Trade-offs

Benefits

  • Fixing it often turns seconds into milliseconds
  • Reduces database connections and load

Costs

  • Over-eager loading can pull huge object graphs
  • Large joins can duplicate parent rows (cartesian explosion) — consider split queries

Use it when

  • Any list page that shows related data
  • GraphQL resolvers that load child entities per parent

Common mistakes

  • Hidden N+1 inside serialisation, when the JSON serialiser touches lazy properties
  • Tests with 3 rows that never reveal the problem
  • Loops calling a repository or remote API per item

Interview questions

Q1How do you detect N+1 queries?
Log SQL in development, use an APM or profiler that counts queries per request, or add a test that asserts the query count for a list endpoint.
Q2How does GraphQL make N+1 more likely, and what fixes it?
Each field resolver runs per parent object, so resolving author for 100 posts means 100 lookups. A DataLoader batches them into one query per request.
Q3Is a single huge JOIN always the best fix?
No. Joining several one-to-many collections multiplies rows. Two or three batched queries (split queries) can be faster.