Skip to content
all notes
4 min read

The N+1 you can't see

DjangoPostgreSQLPerformance

Every Django codebase has one. It passes review, passes tests, and ships — because on a development database with a dozen rows, the difference is single-digit milliseconds. The ORM gives you no signal at all: route.stops.all() reads like an attribute access and behaves like a network call.

The cost is linear in a number that only grows in production. Drag the slider.

12

views.py

routes = Route.objects.all()[:N]
 
for route in routes:
# one DB round-trip per route
render(route.stops.all())

connection.queries

13
Queries13
Modelled time10.2ms
Looks fine. This is the size of your dev database — which is exactly why it ships.

Timings are modelled from a fixed per-query round-trip, not measured against a live database — the shape of the curve is the point, not the absolute numbers.

Why it survives review

The fix is old news. What's worth internalising is the failure mode: the bug is invisible at the size you develop at. Nobody merges this because they don't know about prefetch_related — they merge it because the page felt fast when they checked, and the seed data had twelve rows in it.

That's also why "read the code more carefully" doesn't work as a remedy. The line that costs you 500 queries looks exactly like the line that costs you none. You cannot see the loop from inside the loop.

Make it fail at review time instead

The durable fix isn't the query — it's asserting on the query count, so the regression breaks a test on the branch rather than a dashboard at 2am:

def test_route_list_is_constant_query(client):
    with assertNumQueries(2):
        client.get("/api/routes/")

Two queries, pinned. Add a route with a new relation and forget to prefetch it, and this test fails immediately with a diff that names the exact SQL that got added. It costs three lines and it converts an invisible, size-dependent performance bug into an ordinary red build.

The general shape is worth keeping: when a bug only appears at a scale you don't develop at, stop trying to catch it by looking, and put a number on it that a machine can check.