The N+1 Query Problem Never Actually Left

The N+1 query problem is one of the oldest, best-documented performance bugs in web development, and it still shows up in code review every week. Not because engineers don't know about it — ask anyone with a few years of experience and they can explain it in one sentence. It keeps showing up because the tools that were supposed to make data access easier are also very good at hiding the exact pattern that causes it.

What actually happens

The shape is always the same: one query fetches a list of N records, then a loop over that list triggers one additional query per record to fetch related data — an author for each post, a customer for each order, a manager for each employee. That's N+1 round trips to the database for something that could have been two queries, or one with a join. At ten records it's invisible in local dev. At ten thousand, in production, under load, it's the reason a page that used to load in 200ms now takes eleven seconds.

Why ORMs make it worse, not better

An ORM's whole pitch is that you write post.author.name and don't think about SQL. That's exactly the problem: post.author is a method call that quietly issues a query the first time it's touched, and there's nothing in the syntax that distinguishes "cheap in-memory property access" from "network round trip to the database." A loop that calls .author on each of a thousand posts reads like a thousand cheap accesses. It's a thousand queries. The abstraction that was supposed to hide database complexity succeeded so well that it hid the part where the complexity still exists.

GraphQL didn't fix this, it just moved it

GraphQL resolvers have the exact same failure mode dressed up differently. A naive resolver for Post.author runs one query per post in the result set, because each field resolver is written and executed as if it's independent of every other one — which is true for correctness and false for performance. The fix, batching field resolution through something like DataLoader, exists precisely because this is such a common way to reintroduce N+1 in a system that looks nothing like a traditional ORM.

Why this is getting more common again, not less

AI coding agents are good at producing code that's locally correct — a loop that calls a method for each item in a list is exactly the kind of pattern they generate confidently and often, because it reads clearly and does the right thing functionally. What they don't have automatically is the runtime intuition that related_record.field might be a network call, unless the codebase or the prompt makes that cost explicit. The same review discipline that used to catch this in a human's pull request has to catch it in an agent's, and for the same reason: the code is correct and the query count is invisible until someone runs EXPLAIN or looks at the query log under load.

The fix is visibility, not cleverness

The tools to prevent this are unglamorous and well known: eager loading (includes, select_related, .with()) to fetch related data in a fixed number of queries instead of a variable one, batching data loaders for anything resolver-shaped, and — the part teams skip — a query counter in CI or a slow-query log in staging that fails loudly when a single request issues an unreasonable number of queries. Catching this in code review requires someone to notice a loop with a relationship access inside it, which is exactly the kind of thing that's easy to miss reading a diff and obvious the moment you look at what actually hit the database.

The takeaway

N+1 isn't a bug that got fixed by better ORMs, better query builders, or better frameworks — every generation of tooling has reintroduced it in a new syntax, because the underlying problem was never the syntax. It's that "access a related field" and "make a network call" look identical in code and aren't. Until a codebase makes that distinction visible — through eager loading defaults, batching, or a query budget that fails the build — this bug has a permanent home, no matter which framework is fashionable this year.

Let's talk

Have a project, a role, or just want to chat about this? Send a message.

Get in touch