Cut Django N+1 Queries: When to Use select_related vs prefetch_related
Learn how to eliminate the N+1 problem in Django by choosing select_related for single‑valued relationships and prefetch_related for multi‑valued ones, with a concrete example and verification steps.
30 Apr 2026, 14:44 UTC

The Problem: N+1 Query Explosion
When a view loops over a queryset and accesses a related object inside the loop, Django will issue one query for the main objects and one extra query for each row. With 100 projects, the page might fire 101 or more queries – a classic N+1 problem that scales linearly with data size.
Thesis: Pick the Right Tool for the Relationship
Use select_related for single‑valued relationships (ForeignKey, OneToOneField) and prefetch_related for multi‑valued relationships (ManyToManyField, reverse ForeignKey). When you need a filtered or renamed related set, wrap prefetch_related with a Prefetch object.
select_related – One Query, a SQL JOIN
select_related follows a ForeignKey or OneToOneField and adds a JOIN to the SQL. The database returns the parent and the related rows together, so accessing the related field later does not hit the database again.
Example: Project.objects.select_related('client') will generate SQL similar to:
SELECT project.*, client.* FROM project
JOIN client ON project.client_id = client.id;
Limit the chain depth to what the template actually needs; deep joins can produce wide result sets and duplicate columns.
prefetch_related – Separate Queries, Python Assembly
prefetch_related is for many‑to‑many and reverse foreign keys. Django runs one additional query per relationship, fetches all related rows, and attaches them to the parent objects in Python. This avoids row multiplication that would occur with a JOIN.
Example: Project.objects.prefetch_related('tasks') will execute:
SELECT * FROM project;
SELECT * FROM task WHERE project_id IN (…);
The second query uses a WHERE IN clause to fetch all tasks for the retrieved projects in one shot.
Using Prefetch for Filters or Custom Names
The Prefetch object lets you supply a custom queryset, apply filters, or store the result under a different attribute name:
from django.db.models import Prefetch
projects = Project.objects.prefetch_related(
Prefetch(
'tasks',
queryset=Task.objects.filter(is_completed=False),
to_attr='pending_tasks'
)
)
Now project.pending_tasks is a plain Python list of pending Task instances. Note that it is no longer a QuerySet, so template filters that expect a QuerySet will trigger a new database hit.
Concrete Worked Example – Project Dashboard
# models.py
from django.db import models
class Client(models.Model):
name = models.CharField(max_length=100)
class Project(models.Model):
name = models.CharField(max_length=100)
client = models.ForeignKey(Client, on_delete=models.CASCADE)
class Task(models.Model):
project = models.ForeignKey(Project, related_name='tasks', on_delete=models.CASCADE)
title = models.CharField(max_length=100)
is_completed = models.BooleanField(default=False)
# views.py
from django.shortcuts import render
from django.db.models import Prefetch
from .models import Project, Task
def project_dashboard(request):
projects = (
Project.objects.select_related('client')
.prefetch_related(
Prefetch(
'tasks',
queryset=Task.objects.filter(is_completed=False),
to_attr='pending_tasks'
)
)
)
return render(request, 'dashboard.html', {'projects': projects})
Template snippet:
{% for project in projects %}
{{ project.name }} – {{ project.client.name }}
{% for task in project.pending_tasks %}
- {{ task.title }}
{% empty %}
- No pending tasks
{% endfor %}
{% endfor %}
Verification Steps
- Debug toolbar: With
django-debug-toolbarinstalled, open the page and confirm that the query count stays constant (e.g., 2 queries) regardless of how many projects you have. - Programmatic check:
This snippet will print 0 if prefetching worked. Remember thatfrom django.db import connection initial = len(connection.queries) for p in projects: _ = p.client.name for t in p.pending_tasks: _ = t.title print('Additional queries:', len(connection.queries) - initial)connection.queriesonly records whenDEBUG=True.
Trade‑offs and Caveats
- Deep
select_relatedchains can produce wide result sets and complex SQL; keep chains short. - Filters added after
prefetch_relateddo not affect the prefetched set – usePrefetchwith a filtered queryset instead. - Prefetched data is cached on the instance. If you modify related objects after prefetching, the cached list will not reflect those changes.
- Using
iterator()withprefetch_relatedmay disable the prefetch benefit in some Django releases becauseiterator()bypasses instance caching. - For many‑to‑many relationships,
select_relatedwill not work; you must useprefetch_related.
Actionable Takeaway
1. Identify the relationship type: FK/OneToOne → select_related; M2M/reverse FK → prefetch_related.
2. If you need a filtered or renamed related set, wrap prefetch_related with Prefetch.
3. Verify the query count with a debug toolbar or connection.queries before and after.
4. Keep the dataset realistic when testing; a single‑row test may not reveal N+1 issues.
By following these steps you can keep your Django views fast and scalable, regardless of how many related rows you have.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.