Eliminating N+1 Queries in Django with select_related and prefetch_related
Learn how Django's select_related and prefetch_related turn N+1 query patterns into a constant number of efficient queries, with a concrete Book‑Author‑Genre example and verification steps.
05 Mar 2026, 10:34 UTC

The N+1 problem in a typical Django view
When you iterate over a queryset and access related fields inside the loop, Django issues a separate SQL query for each related lookup. For a list of 100 books, accessing book.author.name and list(book.genre.all()) can generate 1 + 100 × 2 = 201 queries. This N+1 pattern quickly becomes a performance bottleneck as the result set grows.
Thesis: combine select_related for single‑valued relations and prefetch_related for multi‑valued relations
By telling Django to fetch the needed related data up front, you collapse the many small queries into a constant number of larger ones. select_related works for foreign key and one‑to‑one fields via a SQL join; prefetch_related works for reverse foreign key, many‑to‑many, or large related sets by executing a separate query and then merging the results in Python.
When to use select_related
Use it when the related object is guaranteed to exist (or you accept NULLs) and you need it for every row in the main queryset. The join adds columns from the related table to each row, which is efficient when the related set is small.
When to use prefetch_related
Use it for reverse foreign keys (e.g., Author.book_set) and many‑to‑many fields. It avoids inflating the main query with unnecessary columns and keeps the SQL readable, at the cost of an extra query per relationship.
Worked example: Book, Author, Genre models
# models.py
class Author(models.Model):
name = models.CharField(max_length=100)
class Genre(models.Model):
name = models.CharField(max_length=50)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
genres = models.ManyToManyField(Genre, related_name='books')
In a view that displays a list of books with their author name and all genre names, the naïve approach looks like this:
# views.py (naïve)
def book_list(request):
books = Book.objects.all()
return render(request, 'books/list.html', {'books': books})
The template iterates:
{% for book in books %}
{{ book.title }} by {{ book.author.name }} –
Genres: {% for g in book.genres.all %}{{ g.name }}{% if not forloop.last %}, {% endif %}{% endfor %}
{% endfor %}
With the naïve queryset, each loop iteration triggers:
- one query to fetch the author (foreign key)
- one query to fetch the genres (many‑to‑many)
Adding the optimizations reduces the total queries to two:
# views.py (optimized)
def book_list(request):
books = Book.objects.all() \
.select_related('author') \
.prefetch_related('genres')
return render(request, 'books/list.html', {'books': books})
Now Django executes:
- A single query that joins
BookandAuthor(select_related). - A second query that fetches all
Book‑Genrelinks and the relatedGenrerows (prefetch_related).
The Python layer then attaches the prefetched genre collections to each book instance, eliminating the per‑item genre queries.
Trade‑offs and limitations
- Memory overhead:
select_relatedduplicates author columns for every book row;prefetch_relatedloads all related genre objects into memory. For very large related sets (e.g., thousands of genres per book) this can be costly. - Query complexity: Deep joins produced by multiple
select_relatedcalls can hit database limits on join size or produce hard‑to‑read SQL. Eachprefetch_relatedadds an extra query, so chaining many of them still increases the query count, albeit to a small constant. - Incorrect usage: Forgetting to include a relation in
prefetch_relatedor using it on a forward foreign key (whereselect_relatedis appropriate) can lead to unexpected results or no performance gain.
How to verify the improvement
Enable Django’s debug toolbar or set up logging for django.db.backends. In a shell, you can check the query count directly:
from django.db import connection
from myapp.models import Book
# naïve
qs = Book.objects.all()
for b in qs:
_ = b.author.name
_ = list(b.genres.all())
print(len(connection.queries)) # e.g., 201
# reset
connection.queries.clear()
# optimized
qs = Book.objects.all().select_related('author').prefetch_related('genres')
for b in qs:
_ = b.author.name
_ = list(b.genres.all())
print(len(connection.queries)) # e.g., 2
Confirm that the data accessed (book.author.name and genre names) is identical between the two runs to ensure correctness.
Actionable closing
Start by profiling a representative view with the debug toolbar. Identify loops that access foreign key or many‑to‑many fields. Add select_related for every forward foreign key you need and prefetch_related for reverse foreign keys and many‑to‑many relations. Re‑run the profile and verify the query count drops to a small constant. Keep an eye on memory usage when related sets grow large, and consider splitting the queryset or using only/defer if only a subset of columns is required.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.