Skip to main content

N+1 Query Problem

The N+1 query problem fires one query per row to load related data, turning one page load into hundreds of database round-trips. It is the most common ORM performance bug. Fix it with eager loading or batched data loaders.

The N+1 query problem occurs when code runs one query to fetch a collection of N rows, then runs one additional query for each of those rows to load related data. A page that should issue 2 queries instead issues N+1. With 200 records, that is 201 round-trips to the database.

It is the single most common performance defect in applications built on object-relational mappers (ORMs), because the extra queries are invisible in the source code. A simple loop over orders that reads order.customer.name silently triggers a fresh SELECT per iteration.

Why It Happens

ORMs default to lazy loading: a related object is fetched only when first accessed. This is convenient and often correct for a single entity, but inside a loop it explodes. Developers write for order in orders: print(order.customer.name) without realizing each .customer access hits the database. The cost is hidden behind clean, idiomatic code, so it survives code review and only surfaces under production load.

Why It Hurts

Each query carries fixed overhead: network latency, parsing, planning, and connection contention. Hundreds of tiny queries saturate the connection pool and the database's query planner even though the total data volume is small. Latency grows linearly with result-set size, so the feature works in development with 5 test rows and collapses with 5,000 production rows. It is a classic scalability cliff.

Warning Signs

  • The query count in your logs or APM grows in proportion to the number of rows displayed.
  • List or index pages are disproportionately slow compared to detail pages.
  • SQL logs show the same parameterized SELECT repeated with different IDs.
  • Database CPU is dominated by many cheap queries rather than a few expensive ones.

Better Alternatives

Use eager loading to fetch associations in a single additional query or a join: includes, with, JOIN FETCH, or selectinload depending on your ORM. For more complex graphs, adopt batch loading or the data-loader pattern, which collects IDs and resolves them in one batched query. When you only need a few columns, project them directly rather than hydrating full entities.

How to Refactor Out of It

First, make the problem visible: enable SQL logging in development and add query-count assertions to tests so a regression fails the build. Then identify the offending access pattern and add explicit eager loading at the query site. Verify the query count dropped to a constant. For GraphQL or REST APIs that fan out across resolvers, introduce a per-request batching layer. Finally, add monitoring on query-per-request ratios so new N+1 patterns are caught automatically rather than discovered during an incident.