The short answer
Quick answer: The N+1 query problem happens when code runs one query to fetch a list of N items, and then runs one more query per item to fetch related data. Displaying 100 blog posts with their authors becomes 101 database queries when it should be one or two. Each query is fast, but the round trips add up, so pages slow down as data grows. It is most often caused by an ORM's lazy loading. The fix is to fetch the related data up front, using eager loading, a join, or a single batched WHERE id IN (...) query.
What it looks like
A harmless-looking loop:
posts = Post.objects.all()[:100] # 1 query
for post in posts:
print(post.title, post.author.name) # 1 query per post
The SQL that actually runs:
SELECT * FROM posts LIMIT 100;
SELECT * FROM authors WHERE id = 7;
SELECT * FROM authors WHERE id = 12;
SELECT * FROM authors WHERE id = 7;
-- ... 97 more
One query for the list, plus N for the authors: N+1.
Why it is slow
Each of those small queries is quick for the database, perhaps a fraction of a millisecond. The cost is everything around it:
- A network round trip between the application and the database, typically 0.5 to a few milliseconds.
- Parsing and planning each query.
- Building objects for each result.
At 1 ms each, 101 queries cost about 100 ms. With 1,000 items it is a full second. And it nests: posts with comments, each with an author, quickly becomes thousands of queries for one page.
It is easy to miss, because in development you have ten rows and everything feels instant. The problem only shows with production-sized data.
Why ORMs cause it
Object-relational mappers let you write post.author as if the author were already in memory. With lazy loading, the ORM fetches the author only when that attribute is first accessed, and it does so one object at a time.
Lazy loading is convenient and avoids loading data you never use. But inside a loop, or in a template rendering a list, it quietly produces a query per row.
Fix 1: Eager loading
Tell the ORM in advance which relationships you will need. It then fetches them in one or two queries.
| ORM | Using a JOIN | Using a second batched query |
|---|---|---|
| Django | select_related("author") | prefetch_related("comments") |
| Rails | eager_load(:author) | includes(:author) / preload(:author) |
| SQLAlchemy | joinedload(Post.author) | selectinload(Post.comments) |
| Hibernate / JPA | JOIN FETCH | @BatchSize, entity graphs |
| Prisma | include: { author: true } | |
| Laravel Eloquent | with('author') |
posts = Post.objects.select_related("author")[:100] # 1 query with a JOIN
for post in posts:
print(post.title, post.author.name) # no further queries
The Django QuerySet reference and the Rails query guide both document these in detail.
Join or separate query?
- A join returns everything in one query. It is ideal for "to-one" relationships such as a post's author.
- A batched second query runs
SELECT * FROM comments WHERE post_id IN (1, 2, 3, ...)and stitches the results together in memory. It is better for "to-many" relationships, because joining posts to many comments repeats each post's data on every row.
Either way, you go from N+1 queries to one or two.
Fix 2: Write the query yourself
Sometimes the clearest fix is plain SQL that returns exactly what the page needs:
SELECT p.title, a.name, COUNT(c.id) AS comment_count
FROM posts p
JOIN authors a ON a.id = p.author_id
LEFT JOIN comments c ON c.post_id = p.id
GROUP BY p.id, a.name
ORDER BY p.created_at DESC
LIMIT 100;
A common special case: calling .count() on a relationship inside a loop. Replace it with an aggregate in the main query.
Fix 3: Batching in GraphQL
GraphQL APIs are especially prone to N+1, because each field is resolved independently. A query for 50 posts and each post's author runs the author resolver 50 times.
The standard solution is the DataLoader pattern: during one request, collect all the IDs requested, then issue a single batched query and hand each resolver its result. It also caches within the request, so the same author is not fetched twice. This is one of the trade-offs covered in REST vs GraphQL vs gRPC.
It is not only a database problem
The same shape appears anywhere you loop over items and make a call for each:
- Calling another service's API once per item instead of using a bulk endpoint.
- Fetching cache keys one at a time instead of with a multi-get.
- A front end that loads a list and then requests details for every row.
The fix is always the same: batch.
How to detect it
- Log your SQL in development. A wall of near-identical queries differing only by ID is the signature.
- Count queries per request. Tools such as Django Debug Toolbar, Rails' Bullet gem, and Hibernate statistics report this and flag N+1 patterns.
- Use tracing. Application performance monitoring tools show a request with hundreds of tiny database spans.
- Assert in tests. Many frameworks let a test fail if a block runs more than an expected number of queries. Rails can also raise an error on any lazy load (
strict_loading).
Do not over-correct
Eager loading everything has its own costs:
- Loading large relationships you never use wastes memory and time.
- Joining several "to-many" relationships at once multiplies rows (a cartesian product).
- A huge
IN (...)list can itself be slow.
Load what the page displays, no more. And make sure foreign key columns are indexed, or even the batched query will be slow; see how database indexes work. Fewer queries also means less pressure on your connection pool.
Frequently asked questions
Why is it called N+1?
One query fetches the list of N items, and N further queries fetch something for each item, making N+1 in total.
Is lazy loading always bad?
No. It is fine when you access a relationship for a single object, or rarely. It becomes a problem inside loops.
Is one big JOIN always better than several queries?
Not always. For "to-many" relationships, two simple queries often beat a join that duplicates data across many rows.
How many queries per page is too many?
There is no fixed number, but the count should not grow with the number of rows displayed. A page that makes 5 queries for 10 rows should still make about 5 for 1,000.
Conclusion
N+1 is the most common performance problem in database-backed applications, and one of the easiest to fix once you see it. Watch the number of queries your pages run, eager load the relationships you display, and remember the general rule: never do in a loop what you can do in one batch.
Related articles
- How a Database Index Makes Queries 1000x Faster
- How Query Planners Decide the Fastest Way to Run Your SQL
- Why Your Database Needs Connection Pooling
- REST vs GraphQL vs gRPC: Choosing an API Style
