Performance & Latency

N+1 Query Problem

Also known as: N+1 select, N+1 problem

Definition

The N+1 query problem is issuing one query to fetch a list of N rows and then one additional query per row to fetch related data — N+1 round trips where a join or a batched IN clause would need one or two. Latency scales with result-set size, so it passes review with 10 rows and collapses at 1,000.

Last reviewed · Part of the Architecture Glossary

In practice

orders = Order.objects.filter(status="open")   # 1 query
for o in orders:
    print(o.customer.name)                     # 1 query each → N

The cost is round trips, not database work. At 0.4 ms per in-datacentre round trip, 500 orders is 200 ms of pure network — invisible on a laptop against localhost, obvious in production. Cross-AZ or against a proxy, multiply by three.

Fixes by layer:

  • ORM: select_related / prefetch_related (Django), JOIN FETCH (JPA), includes (Rails), Include (EF Core).
  • GraphQL: DataLoader — batch per tick and dedupe by key. GraphQL makes N+1 the default outcome of a nested resolver, which is why every mature server ships a loader.
  • Detection: assert on query count in tests (django-assert-num-queries, Bullet, n_plus_one_control). A count assertion catches the regression the day it is introduced; APM catches it a quarter later.

When it matters

Any list endpoint with nested data, any GraphQL schema with relations, any serialiser that touches a lazy attribute.

Common mistake

Fixing it with a cache. The cache hides the round trips until an invalidation or a cold start, at which point the original 500-query page returns — now during the worst possible traffic conditions.

See also

Go deeper