I’d like to get some thoughts on a small idea for improving developer feedback around N+1 queries in Django applications.
As most of us know, N+1 queries are one of the most common performance issues when working with the ORM. Django already provides tools such as select_related(), prefetch_related(), and QuerySet.explain(), but in practice many developers only discover N+1 problems after using external tools like Django Debug Toolbar or other third-party libraries.
The idea here would be to provide a very lightweight, development-time diagnostic tool that warns when a repeated query pattern suggests a possible N+1 issue.
The basic concept would be:
run only when DEBUG=True
observe query execution using the existing connection.execute_wrappers API
detect repeated SQL templates executed multiple times within a single request
emit a warning through Django’s logging system
be opt-in via middleware, so it has zero impact unless explicitly enabled
For example, when a repeated query pattern is detected, Django could emit a log message like this:
views.py:43 - "SELECT ... WHERE id = %s" executed 25 times - possible N+1 query; consider using select_related().
The idea is simply to provide developers with:
the location where the repeated query originates
the query template that is being executed repeatedly
a clear suggestion that the issue may be related to missing select_related() or similar optimisation
The implementation would remain intentionally minimal, essentially counting repeated SQL templates within a request and logging a warning when a threshold is reached.
If there’s interest in the idea, I’d be happy to prototype a minimal implementation for discussion.
thanks for bringing this up. I also feel like it should be more obvious to prevent n+1 queries.
I would suggest a different approach though:
Introduce some kind of strict mode. E.g. as a queryset method .strict() and / or as a global setting that for example could be enabled during test runs, or as context manager.
In strict mode, no lazy queries to fetch related fields are executed. So either it has been fetched via select_related etc. or an error is raised.
There are other ORMs (e.g. Ecto/Elixir) that work like this per default and therefore don’t have this problem.
It doesn’t catch all n+1 cases compared to your approach. Thinking of choice fields in formsets. But it should be relatively straight forward and easy to implement.
Thanks, this is an interesting idea. A strict mode would be great for enforcing explicit data loading, especially in tests.
I think it’s a bit more opinionated though, and could be harder to adopt in existing codebases. The lightweight detector is more about surfacing potential issues without changing behavior.
Feels like both approaches could complement each other well.