Optimizing Django ORM Queries with select_related
The Django ORM is convenient, but convenience has a cost. Without careful attention, you’ll create N+1 query problems that destroy performance.
Let’s fix that.
The N+1 Problem
# models.py
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
Innocent-looking code:
books = Book.objects.all()
for book in books:
print(f"{book.title} by {book.author.name}")
What happens:
-- 1 query to get books
SELECT * FROM book;
-- N queries to get each author (one per book!)
SELECT * FROM author WHERE id = 1;
SELECT * FROM author WHERE id = 2;
SELECT * FROM author WHERE id = 3;
-- ... N more queries
With 1000 books, that’s 1001 queries. Your page takes 10 seconds to load.
select_related: The ForeignKey Solution
select_related performs a SQL JOIN, fetching related objects in a single query:
books = Book.objects.select_related('author').all()
for book in books:
print(f"{book.title} by {book.author.name}") # No additional queries!
-- Single query with JOIN
SELECT book.*, author.*
FROM book
INNER JOIN author ON book.author_id = author.id;
Chaining Relations
# models.py
class Publisher(models.Model):
name = models.CharField(max_length=100)
class Author(models.Model):
name = models.CharField(max_length=100)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
# Follow the chain
books = Book.objects.select_related('author__publisher').all()
for book in books:
# All of this is already loaded
print(f"{book.title} by {book.author.name}")
print(f"Published by {book.author.publisher.name}")
Multiple Relations
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
editor = models.ForeignKey(Editor, on_delete=models.CASCADE)
books = Book.objects.select_related('author', 'editor').all()
prefetch_related: The ManyToMany/Reverse Solution
select_related works for ForeignKey and OneToOne. For ManyToMany and reverse relations, use prefetch_related:
class Book(models.Model):
title = models.CharField(max_length=200)
authors = models.ManyToManyField(Author) # Many authors per book
# prefetch_related does separate queries, then joins in Python
books = Book.objects.prefetch_related('authors').all()
for book in books:
for author in book.authors.all(): # Already prefetched!
print(f"{book.title} by {author.name}")
-- Query 1: Get books
SELECT * FROM book;
-- Query 2: Get all related authors
SELECT author.*, book_authors.book_id
FROM author
JOIN book_authors ON author.id = book_authors.author_id
WHERE book_authors.book_id IN (1, 2, 3, ...);
Two queries instead of N+1.
Reverse Relations
# Get all authors and their books (reverse ForeignKey)
authors = Author.objects.prefetch_related('book_set').all()
for author in authors:
for book in author.book_set.all(): # Prefetched!
print(book.title)
Prefetch Objects: Fine-Grained Control
For complex cases, use Prefetch objects:
from django.db.models import Prefetch
# Only prefetch published books
authors = Author.objects.prefetch_related(
Prefetch(
'book_set',
queryset=Book.objects.filter(published=True).order_by('-pub_date')
)
)
# Use a custom attribute name
authors = Author.objects.prefetch_related(
Prefetch(
'book_set',
queryset=Book.objects.filter(published=True),
to_attr='published_books' # Access as author.published_books
)
)
for author in authors:
for book in author.published_books: # List, not QuerySet
print(book.title)
Combining Both
books = Book.objects.select_related(
'author',
'publisher'
).prefetch_related(
'tags',
'reviews'
).all()
Use select_related for ForeignKey/OneToOne (JOIN).
Use prefetch_related for ManyToMany/reverse (separate queries).
Debugging: Django Debug Toolbar
Install django-debug-toolbar to see all queries:
# settings.py
INSTALLED_APPS = [..., 'debug_toolbar']
MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware', ...]
INTERNAL_IPS = ['127.0.0.1']
The SQL panel shows query count and time. Watch for duplicate queries.
Common Patterns
In Views
def book_list(request):
books = Book.objects.select_related('author').all()
return render(request, 'books/list.html', {'books': books})
In Serializers (DRF)
class BookViewSet(viewsets.ModelViewSet):
serializer_class = BookSerializer
def get_queryset(self):
return Book.objects.select_related('author').prefetch_related('tags')
In Admin
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_display = ['title', 'author_name']
list_select_related = ['author']
def author_name(self, obj):
return obj.author.name
When Not to Optimize
Don’t blindly add select_related everywhere:
- Large related objects: JOINs fetch all columns
- Rarely accessed relations: Only optimize hot paths
- Complex queries: Sometimes raw SQL is clearer
Profile first. The Debug Toolbar shows where the problems are.
Final Thoughts
N+1 queries are the most common Django performance issue. The fix is simple: use select_related for forward relations, prefetch_related for reverse and M2M.
Make it a habit to think about queries when writing views. Your users (and your database) will thank you.
Every query counts.