select_related vs prefetch_related: Fixing Django N+1 Queries
Stop N+1 queries in Django by choosing select_related for foreign keys and prefetch_related for many-valued relations, then verify with SQL logging.
12 Sept 2025, 02:41 UTC

A slow post list and the N+1 problem
Imagine a template that loops through blog posts and displays each post's author and tags. With a naive queryset, Django issues one query to fetch the posts, then one extra query for every post's author and another for every post's tags. Ten posts can mean twenty-one database round trips. This is the classic N+1 query problem, and it is one of the most common reasons a Django page slows down as data grows.
The fix is usually not more indexes or caching. It is telling Django to fetch related rows up front. Django offers two tools for this: select_related and prefetch_related. They sound similar, but they operate differently and apply to different relationship types.
select_related: one query with a SQL JOIN
select_related follows single-valued relationships—ForeignKey and OneToOneField—by issuing a SQL JOIN and returning the related row in the same result set. If every post has exactly one author, Django can fetch posts and authors together.
Because the database does the joining, the related object is already attached when you access it in Python, so no additional query fires inside the template loop.
prefetch_related: separate lookups joined in Python
prefetch_related handles many-valued relationships such as ManyToManyField and reverse foreign-key relations. Django runs one query for the main objects, then one or more separate queries for the related objects, and joins them in memory.
It does not collapse everything into a single query. It trades extra round trips for fewer total queries than the naive loop, and it keeps the result rows narrower than a large multi-table join.
Worked example: posts, authors, and tags
Suppose you have these models:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
tags = models.ManyToManyField("Tag", blank=True)
class Tag(models.Model):
name = models.CharField(max_length=50)
A template that shows {{ post.author.name }} and loops through post.tags.all() triggers N+1 queries unless you prepare the queryset:
posts = Post.objects.select_related("author").prefetch_related("tags").all()
select_related("author") joins the authors table once. prefetch_related("tags") fetches tags in a separate query and attaches them to the correct posts in Python. The queryset stays lazy, so you can still add filters, only, or defer after these methods.
Where to run and what to check
Run the version check in your project's virtual environment:
python -c "import django; print(django.get_version())"
This requires no special permissions. Compare the printed version against the Django release notes for your installed release; the behavior described here is stable across Django 3.2 LTS, 4.2 LTS, and 5.x. Risk: activating the wrong environment prints the system Django version instead of the project's dependency.
To verify the SQL, enable Django's debug toolbar or add SQL logging to your project settings. Then inspect str(queryset.query) in a shell or view. With select_related, expect to see JOIN clauses in the SQL. With prefetch_related, expect separate queries without those joins in the main query.
Trade-offs and limits
select_relatedcan produce very wide rows and large result sets when you join many tables or traverse deep relationship chains. Only select the relationships you actually need.prefetch_relateddoes not reduce the total query count to one. It executes additional queries and holds related objects in memory, which matters for very large collections.- Neither method prefetches relationships you do not name. Calling
post.tags.all()afterselect_related("author")still triggers a query unless you also prefetched tags.
Check the result before you ship
Load the page with the debug toolbar open and compare the query count before and after adding the two methods. You should see the per-post author and tag queries disappear, replaced by one joined query and one tags query. If the count does not drop, check that you applied the methods to the queryset actually evaluated in the view, and that the template is not accessing unrelated fields.
Start with select_related for single-valued relations and prefetch_related for collections. Combine them when a model has both, then verify with SQL logging rather than guessing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.