N+1 query problem

Also known as: N+1 problem

On this page 7

The N+1 query problem is a database performance antipattern in which an application runs one query to fetch a list of N records and then one additional query per record to load related data, producing N+1 round trips where one or two would suffice. In databases accessed through an application layer, it is one of the most common causes of pages that are fast in development and slow in production.

The pattern is also called the N+1 problem. It rarely shows up in the SQL itself, because each individual statement is cheap and well indexed. The cost lies in the number of round trips, the per-query overhead on the server and the network latency that accumulates with every extra statement.

How the N+1 query problem arises

The N+1 query problem arises when code iterates over a result set and touches a related object inside the loop. A typical case is an object-relational mapper (ORM) with lazy loading: the first query returns 200 orders, and reading order.customer inside the loop silently issues SELECT * FROM customers WHERE id = ? once per order.

The ORM is doing exactly what it was configured to do. Lazy loading defers related data until it is accessed, which saves work when the relation is never used but becomes expensive inside loops. The same pattern appears without an ORM, for example in template code, GraphQL resolvers or service layers that call a per-item lookup function for every element of a list.

The following table breaks down where the time goes for a single extra query, which explains why many small queries are slower than one larger query.

ComponentTypical rangeWhat it depends on
Network round trip0.1-1 ms in one data center, 10-100 ms across regionsDistance between application and database
Parsing and planningTens of microseconds to a few millisecondsPrepared statements, plan cache, query complexity
ExecutionOften under 1 ms for an indexed lookupIndex use, buffer pool hit rate
Driver and ORM overheadMicroseconds to millisecondsObject hydration, logging, connection handling
Cost components of one additional query

Why the cost grows with N

The total cost of the N+1 query problem grows linearly with the number of parent rows, so a page that issues 11 queries for 10 items issues 1,001 queries for 1,000 items. At 1 ms per round trip, that adds roughly one second of pure waiting, before any rendering or business logic runs.

Nested relations make it worse. If each order also loads its line items, and each line item loads its product, the pattern becomes N times M additional queries. This multiplicative form is sometimes called an N+1 cascade, and it can push a single request into tens of thousands of statements.

The load also falls on the database server. Each query consumes a connection slot, CPU time for parsing and protocol handling and space in logs and monitoring. Under concurrency, the extra traffic can exhaust connection pooling limits and create queueing that slows unrelated requests.

Detecting the pattern

The N+1 query problem is detected by counting queries per request, not by looking at individual query durations. Each statement looks fast in isolation, so a slow query log with a threshold of 100 ms will usually miss it entirely.

Several signals reveal the pattern reliably. The table summarizes the most useful ones and what to look for.

SignalWhere to observe itTypical pattern
Query count per requestORM debug logs, request profilersCount grows with page size
Repeated statement shapeStatement statistics, query digest viewsSame query text with different parameters
Trace waterfallDistributed tracing spansLong staircase of short database spans
Latency versus data volumeLoad tests, production metricsResponse time rises with list length
Signals that indicate an N+1 query problem

A practical safeguard is an automated query budget in tests: the test fails when an endpoint exceeds a fixed number of queries, for example 10, regardless of how many records the fixture creates. Some ORMs and plugins can also warn on lazy loads inside loops or raise an error when strict loading is enabled.

Ways to fix the N+1 query problem

The fix is to fetch related data in bulk, so that the number of queries stays constant as N grows. Most solutions reduce N+1 queries to one or two queries in total.

The approaches differ in how they shape the result and when they fit best, as the following comparison shows.

FixQueries issuedBest suited forMain trade-off
Join with eager loading1To-one relations such as order to customerDuplicated parent columns for to-many relations
Batch loading with IN list2To-many relations and large listsVery long IN lists need chunking
DataLoader-style batching1 per relation levelGraphQL resolvers and service layersRequires a request-scoped batching layer
Aggregation in SQL1Counts, sums and summaries per parentLogic moves from code into the query
Denormalized or cached field1Hot read paths with stable dataExtra write work and staleness risk
Common fixes and their trade-offs

In ORM terms, eager loading appears under names such as include, prefetch_related, select_related, joinedload or JOIN FETCH. A join works well for to-one relations; for to-many relations, a separate batched query avoids the row multiplication that a join produces, often called a Cartesian explosion.

Batch loading collects all parent keys and issues one WHERE parent_id IN (...) query. Databases and drivers limit parameter counts, often in the range of a few thousand to about 65,000 parameters, so very large lists should be split into chunks of a few hundred to a few thousand keys.

The N+1 query problem is a round-trip problem, which distinguishes it from issues that query optimization or an execution plan review would find. Each query in the pattern usually has a good plan; the waste is in how many times it runs.

IssueRoot causeMain remedy
N+1 query problemToo many small queriesBulk or eager loading
Slow single queryPoor plan or missing indexIndexing and query rewriting
Lock contentionTransactions waiting on each otherShorter transactions, better ordering
Over-fetchingLoading columns or rows that are never usedNarrower selects and pagination
How the N+1 query problem differs from nearby issues

Fixes have their own pitfalls. Eager loading everything by default replaces many small queries with large ones that fetch unused data. A query cache can hide the symptom in testing while production traffic, with more distinct keys, still pays the full price.

  • Fix only measured hot paths, guided by query counts, not by guesswork.
  • Paginate parent lists so that N has a predictable upper bound.
  • Test with realistic data volumes, because ten fixture rows hide the pattern.
  • Recheck after schema changes, since new relations can reintroduce lazy loads.

Stripped to the essentials

Imagine asking a librarian for a list of 100 books and then walking back to the desk 100 separate times to ask who wrote each one. Every question is quick, but the walking adds up. Asking once for the list together with the authors would take a single trip.

The N+1 query problem is that walking. A program asks the database for a list and then asks again for every item on it. The fix is to ask for everything needed in one or two requests, so the waiting no longer grows with the length of the list.

Where it is applied

An online store shows an order history page with 50 orders per page. Each order displays the customer name and the number of items. The original code loads the orders in one query, then fetches the customer and counts the items separately for each order, which results in 101 queries per page view.

In development, with a local database and 5 sample orders, the page loads in about 40 ms. In production, the database sits in a different availability zone with roughly 1 ms of latency per round trip, and the page takes over 300 ms under load. A request profiler shows the same two statements repeated 50 times each.

The team switches to eager loading for customers and a single grouped count query for items. The page now issues three queries regardless of page size, response time drops to about 30 ms, and a query budget test keeps the pattern from returning.