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.
| Component | Typical range | What it depends on |
|---|---|---|
| Network round trip | 0.1-1 ms in one data center, 10-100 ms across regions | Distance between application and database |
| Parsing and planning | Tens of microseconds to a few milliseconds | Prepared statements, plan cache, query complexity |
| Execution | Often under 1 ms for an indexed lookup | Index use, buffer pool hit rate |
| Driver and ORM overhead | Microseconds to milliseconds | Object hydration, logging, connection handling |
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.
| Signal | Where to observe it | Typical pattern |
|---|---|---|
| Query count per request | ORM debug logs, request profilers | Count grows with page size |
| Repeated statement shape | Statement statistics, query digest views | Same query text with different parameters |
| Trace waterfall | Distributed tracing spans | Long staircase of short database spans |
| Latency versus data volume | Load tests, production metrics | Response time rises with list length |
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.
| Fix | Queries issued | Best suited for | Main trade-off |
|---|---|---|---|
| Join with eager loading | 1 | To-one relations such as order to customer | Duplicated parent columns for to-many relations |
| Batch loading with IN list | 2 | To-many relations and large lists | Very long IN lists need chunking |
| DataLoader-style batching | 1 per relation level | GraphQL resolvers and service layers | Requires a request-scoped batching layer |
| Aggregation in SQL | 1 | Counts, sums and summaries per parent | Logic moves from code into the query |
| Denormalized or cached field | 1 | Hot read paths with stable data | Extra write work and staleness risk |
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.
Related concepts and common pitfalls
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.
| Issue | Root cause | Main remedy |
|---|---|---|
| N+1 query problem | Too many small queries | Bulk or eager loading |
| Slow single query | Poor plan or missing index | Indexing and query rewriting |
| Lock contention | Transactions waiting on each other | Shorter transactions, better ordering |
| Over-fetching | Loading columns or rows that are never used | Narrower selects and pagination |
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.