iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
To find and fix N+1 queries in Django before production, reproduce the full operation that builds the response, inspect repeated SQL and its callers, choose a loading strategy that matches the relationship, then remeasure and add a query-count regression test. The right fix depends on how related data is accessed; fewer queries alone do not guarantee lower latency.
What the N+1 queries problem looks like
An N+1 pattern occurs when code retrieves a set of objects, then performs another database lookup for each object to access related data. The total can grow to one query for the initial set plus N additional queries. Django’s database optimization guide recommends identifying which queries run and what they cost before optimizing.
For example, a page that loads a list of articles and then reads each article’s author may execute one query for the articles and a repeated author query for every article. The repeated SQL typically has the same structure but a different parameter. A high query count by itself does not establish an N+1: look for that repetition and trace it to the code path that accesses the relation.
How to find N+1 queries in Django
1. Reproduce the complete response path
Exercise the real view, serializer, template, or service operation that produces the result. A queryset may look efficient in isolation while later code triggers related-object access. Templates can do this indirectly, so inspect the rendered response path rather than stopping at queryset construction.
#1 Best Overall
2. Measure queries and timings
In local development, Django Debug Toolbar’s SQL panel shows query text and execution times and can link to EXPLAIN. Use it while exercising the relevant request, then compare repeated query shapes and their parameters. The Django optimization guide also documents QuerySet.explain() for inspecting how the selected database executes a query, including joins and indexes. Interpret its output for your backend and actual workload.
The SQL panel documentation describes the panel’s capabilities. Toolbar data is gathered during request handling, and collection or rendering overhead can become noticeable in some projects; its tips and configuration guide discuss configuration-dependent behavior. Treat it as a development diagnostic, not proof that a change is production-ready.
3. Trace each repeated query to its caller
Follow the repeated SQL back to the relation access in Python, a template, or serialization code. Confirm whether each object triggers its own lookup and whether the consuming code needs a single related object or a collection. That distinction determines which loading strategy to consider.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Rank #2
Choose a loading strategy that matches the relation
Django’s QuerySet API reference distinguishes joined loading from batched related lookups. Check the documentation for the Django version your project runs, and compare the resulting query count, loaded data, memory use, and latency on representative data.
| Strategy | Best fit | How it loads data | Important trade-off |
|---|---|---|---|
select_related() |
Single-valued foreign-key and one-to-one relations | Retrieves related objects in the same query using a SQL join | It is limited to single-valued relationships; joining across “many” relations can multiply result rows. |
prefetch_related() |
Multi-valued relations, including many-to-many relationships | Runs the primary query and additional related lookup(s), then joins results in Python | It fully loads the primary results and specified related objects into memory; broad prefetches and large IN clauses may have costs. |
Use select_related() for a single related object
When each object needs a foreign-key or one-to-one value, consider joining it into the original query:
articles = Article.objects.select_related("author")
This suits access such as article.author. Check the actual query and workload rather than adding every possible relation to the join.
Use prefetch_related() for a collection
For a collection such as a many-to-many relation, consider fetching related rows in a separate batched query:
pizzas = Pizza.objects.prefetch_related("toppings")
This is suited to consuming the prefetched relation itself, such as pizza.toppings.all(). Prefetching is targeted: a later call such as pizza.toppings.filter(...) issues a new query, because that filtered result is different from the cached relation. For a filtered subset, use a targeted Prefetch queryset and, where appropriate, a clear destination with to_attr.
Prefetch lookups run after the primary queryset begins evaluation. Concurrent database changes between those queries can make the combined result appear inconsistent. Consider whether the extra rows in memory and the related lookup’s size are acceptable for the operation.
Consider FETCH_PEERS only when the deployed version supports it
Django 6.1 documents the FETCH_PEERS fetch mode, which batches missing fields for peer instances; Django says it can reduce many N+1 cases to two queries. This is version-sensitive: verify support in your deployed Django version and assess whether the peer-field access pattern fits before using it. See Django’s fetch modes documentation.
Verify the fix and prevent regressions
Recheck behavior, not just the query count
Run the same response path with representative data after changing the queryset. Confirm that the output is correct, inspect the new SQL, and compare execution time as well as query count. Joined or prefetched loading may retrieve more data or use memory differently, so the best result depends on the workload and database.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsAdd a query-count test around the operation
Django’s assertNumQueries() can check the number of queries executed by a block. Use enough representative objects to exercise the repeated access pattern; a one-object fixture may not expose a per-object query. Set the expected count for the tested operation and its test setup rather than treating any count as universal.
Best Value
with self.assertNumQueries(expected_count):
response = self.client.get("/articles/")
The count will depend on the view and test setup. Consult the current testing documentation for the version in use.
Check behavior under real deployment conditions
Local instrumentation helps locate code patterns, while direct database monitoring can reveal query behavior under production traffic and data. Django’s optimization guidance includes direct database monitoring among the tools to consider. Keep diagnostic instrumentation appropriate to the environment, and judge a change using the actual workload rather than query count alone.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
Recommended Free Tools

