When you first start building with Django its ORM feels like magic. You write simple Python code and it generates complex SQL for you. But as your application grows and you add more data you might notice things start to slow down. Pages that were once instant now take seconds to load.
The cause is often not your Python code or your database server. It’s a simple and common mistake in how you’re using the ORM. The problem is called the N+1 query bug and once you learn to see it you will see it everywhere. The good news is that Django gives you two powerful tools to fix it select_related
and prefetch_related
.
Let’s imagine a simple blog application. We have two models Author
and Post
. Each Post
has one Author
.
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
def __str__(self):
return self.title
Now imagine you want to display a list of the 10 most recent posts and their authors. Your view might look something like this.
def latest_posts(request):
posts = Post.objects.order_by('-published_date')[:10]
return render(request, 'latest_posts.html', {'posts': posts})
And in your template you loop through the posts.
<ul>
{% for post in posts %}
<li>{{ post.title }} by {{ post.author.name }}</li>
{% endfor %}
</ul>
This code looks innocent. And it works. But it hides a serious performance problem.
When you run this you will see one SQL query to get the 10 posts.
SELECT * FROM “blog_post” ORDER BY “published_date” DESC LIMIT 10;
But then inside the loop for every single post Django needs to find the author’s name. To do that it has to run another query to get the author from the blog_author
table.
SELECT * FROM “blog_author” WHERE “id” = 1; SELECT * FROM “blog_author” WHERE “id” = 5; SELECT * FROM “blog_author” WHERE “id” = 2; ... and so on for each post.
So you have 1 query to get the posts plus N queries to get the author for each post. That’s a total of 1 + 10 = 11 queries to render this simple page. If you were showing 100 posts it would be 101 queries. This is the N+1 query problem and it can bring your application to its knees.
select_related
Django provides select_related
to solve this exact problem for foreign key and one to one relationships. It tells the ORM to fetch the related objects in the same initial database query using a SQL JOIN.
Let’s fix our view.
def latest_posts(request):
posts = Post.objects.select_related('author').order_by('-published_date')[:10]
return render(request, 'latest_posts.html', {'posts': posts})
The only change is adding .select_related('author')
. Now when Django runs this code it generates a single more efficient SQL query.
SELECT “blog_post”.“id”, “blog_post”.“title”, “blog_author”.“id”, “blog_author”.“name” FROM “blog_post” INNER JOIN “blog_author” ON (“blog_post”.“author_id” = “blog_author”.“id”) ORDER BY “published_date” DESC LIMIT 10;
With this one change we went from 11 queries down to 1. The database does the work of joining the tables which it’s very good at. When you access post.author.name
in the template Django doesn’t need to go back to the database. The author’s data is already there on the post object.
Use select_related
when the object you want to retrieve is a single object like a ForeignKey
or a OneToOneField
.
prefetch_related
So select_related
is great for single related objects. But what about many to many relationships or when you need to go in the reverse direction like getting all posts for an author?
Let’s flip our example. We want to list authors and all the posts they have written.
A naive approach would look like this.
# In a view
authors = Author.objects.all()
# In a template
{% for author in authors %}
<h2>{{ author.name }}</h2>
<ul>
{% for post in author.post_set.all %}
<li>{{ post.title }}</li>
{% endfor %}
</ul>
{% endfor %}
This again causes an N+1 problem. You get 1 query for all authors. Then for each author you get another query for all their posts.
You cannot use select_related
here because it only works for single objects not sets of objects. This is where prefetch_related
comes in.
prefetch_related
works differently. Instead of doing a single big JOIN it runs a separate query for the related items and then joins the data in Python.
Let’s fix our query.
authors = Author.objects.prefetch_related('post_set').all()
Now Django performs just two queries.
SELECT * FROM “blog_author”; SELECT * FROM “blog_post” WHERE “author_id” IN (1, 2, 3, 4, ...);
First it gets all the authors. Then it gets all the posts related to any of those authors in a single second query. It then processes this data in memory and attaches the correct posts to each author. The result is the same but the performance is vastly better.
The distinction is simple once you get it.
select_related
when you are accessing a single related object. This translates to a ForeignKey
or OneToOneField
. It results in a single database query with a JOIN.prefetch_related
when you are accessing a set of related objects. This is for ManyToManyField
or reverse ForeignKey
lookups. It results in two database queries and a Python join.Becoming skilled at using these two methods is a major step in writing professional Django applications. The best way to find these issues is to use a tool like the Django Debug Toolbar. It shows you every query your page makes. If you see a long list of repeated queries you probably have an N+1 problem.
Mastering select_related
and prefetch_related
will make your applications faster and your users happier.
— Rishi Banerjee
September 2025