Cut Django ORM N+1 Queries: Mastering select_related and prefetch_related
Eliminate Django N+1 queries by mastering select_related for FK/OneToOne and prefetch_related for M2M. See a concrete example, trade‑offs, and how to profile before deploying.
16 Feb 2026, 12:30 UTC

What’s the Problem?
When you iterate over a queryset and access a related object, Django often fires a new SQL query for each instance. This N+1 query pattern can turn a simple page render into dozens of round‑trips, especially on large datasets. The classic symptom is a spike in the query count shown by the Django Debug Toolbar.
Thesis: Use the Right Tool for the Right Relationship
Two ORM helpers exist to flatten this pattern:
select_related– performs a SQLJOINand pulls the related row into the same result set. Use it forForeignKeyandOneToOneField.prefetch_related– runs a separate query for the related set and stitches the objects together in Python. Use it forManyToManyFieldand reverseForeignKeylookups.
1. How They Work Under the Hood
select_related expands the original query with a LEFT OUTER JOIN. The database returns a wider row, but you only hit the DB once.
prefetch_related executes two queries: the main one and one for each related set. After both results are fetched, Django builds a dictionary keyed by the foreign key and attaches the related objects in memory.
| Method | SQL Strategy | Typical Use |
|---|---|---|
| select_related | JOIN | ForeignKey, OneToOne |
| prefetch_related | Separate Query + Merge | ManyToMany, reverse FK |
2. Worked Example
Consider these models:
# models.py
class Author(models.Model):
name = models.CharField(max_length=100)
class Tag(models.Model):
label = models.CharField(max_length=30)
class BlogPost(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag)
Naïve view code:
# views.py
posts = BlogPost.objects.all()
for post in posts:
print(post.title, post.author.name) # N+1 on author
for tag in post.tags.all(): # N+1 on tags
print(tag.label)
Result: 1 query for posts, +1 per post for author, +1 per post for tags – easily 1 + 2N queries.
Optimised with select_related and prefetch_related:
# views.py
posts = (BlogPost.objects
.select_related('author')
.prefetch_related('tags'))
for post in posts:
print(post.title, post.author.name)
for tag in post.tags.all():
print(tag.label)
Now the DB sees only two queries: one for posts + author, one for tags.
To verify, run in the Django shell:
$ python manage.py shell
>>> from myapp.models import BlogPost
>>> qs = BlogPost.objects.select_related('author').prefetch_related('tags')
>>> print(qs.query)
>>> print(qs.count())
Use the Debug Toolbar’s SQL panel to confirm the query count is 2.
3. Trade‑offs and Limitations
- select_related increases the size of each row. If the joined table has many columns, the result set can become large and memory‑heavy.
- prefetch_related materialises all related objects in memory. With a very large tag set, this can consume significant RAM and may be slower than lazy loading when the related set is rarely accessed.
- Both methods require that the related objects fit into a single query’s
SELECTlist; complex annotations or filtering on the related set may still trigger additional queries. - Using both on the same relationship (e.g.,
select_related('author').prefetch_related('author')) is redundant and can confuse query plans.
4. Actionable Next Steps
- Enable Django Debug Toolbar in the development environment to see query counts per view.
- Profile in Production – use
django-silkordjango-debug-toolbarin a staging environment to capture real traffic patterns. - Run
.queryset.explain()on suspect querysets to see the execution plan and confirm that joins are used. - Measure performance before and after adding
select_related/prefetch_relatedwithtimeitor a simpleprint(time.perf_counter())around the view. - Apply the optimisations incrementally: first eliminate the obvious N+1 on ForeignKey lookups, then tackle ManyToMany if the query count remains high.
- Keep an eye on memory usage; if
prefetch_relatedcauses out‑of‑memory errors, consider limiting the prefetch withPrefetch('tags', queryset=Tag.objects.only('id', 'label')).
Closing Thoughts
Profiling early and applying select_related for single‑valued relationships and prefetch_related for multi‑valued ones usually cuts query counts from dozens to a handful. Remember the trade‑offs: larger rows vs. more memory. Iterate based on real metrics, not intuition, and your Django app will stay snappy even as the data grows.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.