The N+1 query problem happens when code fetches a list of N records, then loops over them and fires one additional query per record to get related data — N+1 total queries where one or two would have done the job. It is one of the most common and most invisible performance bugs, because it works fine in development with ten test rows and quietly falls over in production with ten thousand.
What changed in 2026
- ORMs (Prisma, Django ORM, ActiveRecord, SQLAlchemy) shipped better built-in N+1 detection, warning or logging in development mode when a loop is about to trigger per-row queries.
- Dataloader-style batching, popularized by GraphQL, became common outside GraphQL contexts too, as a general pattern for collapsing per-item fetches into single batched queries.
- Observability tools improved at surfacing N+1 patterns directly from production query traces, rather than requiring a developer to notice the query count by hand.
How the problem shows up
A typical example: fetch a list of 50 blog posts, then for each post, fetch its author with a separate query. That is 1 query for the posts plus 50 queries for the authors — 51 queries where a single JOIN or a single batched IN query would return everything needed in one or two round trips. The code often looks entirely reasonable, since the loop and the per-item fetch are written naturally, one at a time.
// N+1: one query per post
const posts = await db.posts.findMany();
for (const post of posts) {
post.author = await db.users.findUnique({ where: { id: post.authorId } });
}
Fixes compared
| Fix |
How it works |
Best for |
| Eager loading / JOIN |
Fetch related data in the same query using a JOIN |
Relational data with a clear one-to-one or one-to-many shape |
| Batched IN query |
Collect all needed IDs, fetch related rows in one WHERE id IN (...) query |
ORMs where a JOIN would duplicate rows awkwardly |
| Dataloader / batching layer |
Queue individual requests within a tick, then issue one batched fetch |
GraphQL resolvers, or any per-field resolution pattern |
| Caching |
Store frequently accessed related records in memory or a cache layer |
Data that changes rarely relative to how often it is read |
Why it is easy to miss
N+1 bugs pass code review because the code reads cleanly — a for loop with a natural-looking fetch inside it. They pass local testing because ten rows means eleven queries, which is fast enough not to notice. The problem only becomes visible under realistic data volume or concurrent load, which is exactly the environment where developers are least likely to be watching query counts directly. This is part of why connection pooling and query monitoring, covered in the connection pooling guide, matter as a production safety net even when application code has bugs like this.
Common mistakes
- Fixing it in one place and missing the pattern elsewhere. N+1 is a shape of code, not a single line; the same mistake tends to repeat across a codebase until eager loading becomes a habit.
- Over-correcting with eager loading everywhere. Always joining every relation, even when unused, adds unnecessary query weight and can itself become a performance problem.
- Not testing with realistic data volume. A staging environment with production-scale data catches N+1 patterns that a ten-row dev database will not.
- Ignoring query logs. Most ORMs can log every query executed; reviewing that log during a code review catches N+1 patterns before they reach production.
FAQ
Is the N+1 problem specific to ORMs?
It is most common with ORMs because they make per-record fetching so easy to write accidentally, but the same pattern can happen with raw SQL or any data-fetching code that loops and queries per item.
Does GraphQL cause N+1 problems?
GraphQL does not cause it directly, but its field-by-field resolver model makes it easy to introduce N+1 patterns unintentionally, which is why dataloader-style batching became closely associated with GraphQL server implementations.
How do I detect N+1 queries in my app?
Turn on query logging in development and watch the query count for a given request. Most ORMs and APM tools can also flag a suspicious repeated-query pattern automatically.
Is a JOIN always the right fix?
Not always. A JOIN can duplicate parent rows when the relation is one-to-many, requiring extra deduplication logic. A separate batched query is sometimes cleaner even though it is technically two queries instead of one.
Where to go next