Choosing Between select_related and prefetch_related in Django ORM
Learn when to use select_related versus prefetch_related to cut N+1 queries in Django, with a concrete example and verification steps.
28 Aug 2025, 10:47 UTC

Problem: extra queries hurting your list view
Imagine a simple blog where each post shows its author (a foreign key) and a list of tags (a many‑to‑many relationship). When you render 20 posts in a template, Django will issue one query to fetch the posts, then for each post an extra query to get the author and another to fetch the tags. That’s 1 + 2 × 20 = 41 queries – a classic N+1 problem that slows page load and wastes database connections.
Thesis: pick the right prefetch technique for the relationship type
Django provides two ORM helpers that collapse those extra hits into a constant number of queries: select_related for single‑valued relations (foreign key, one‑to‑one) and prefetch_related for multi‑valued relations (many‑to‑many, reverse foreign key). Using them correctly turns the query count from O(N) to O(1) for the related data.
How select_related works
When you call select_related('author') on a queryset, Django adds a SQL JOIN to the original query so the related author columns are fetched in the same row. No extra query is needed later when you access post.author.
How prefetch_related works
prefetch_related('tags') works differently. Django first runs the base query to get the posts, then runs a second query that fetches all tags for those posts. It stores the results in a cache and, when you later access post.tags.all(), Django returns the cached collection instead of hitting the database again.
Worked example: models, querysets and query counts
# models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Tag(models.Model):
name = models.CharField(max_length=50)
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag, related_name='posts')
published_at = models.DateTimeField()
# views.py (simplified)
def post_list(request):
# Without optimization
posts = Post.objects.all() # will cause N+1
return render(request, 'posts/list.html', {'posts': posts})
# With optimization
# posts = Post.objects.select_related('author').prefetch_related('tags')
To see the difference, enable SQL logging in a Django shell:
import logging
logging.getLogger('django.db.backends').setLevel(logging.DEBUG)
from myapp.models import Post
# 1️⃣ No optimization
list(Post.objects.all())
# You will see: one query for posts, then for each post a query for author and a query for tags.
# 2️⃣ With select_related and prefetch_related
list(Post.objects.select_related('author').prefetch_related('tags'))
# You will see: one query that joins posts and authors, plus a second query that fetches all tags.
If you prefer a visual tool, run the development server with the Django Debug Toolbar enabled. The SQL panel will show:
- Without optimization: many repeated queries similar to
SELECT ... FROM myapp_author WHERE id = …andSELECT ... FROM myapp_tag INNER JOIN …. - With optimization: a single
SELECT ... FROM myapp_post LEFT OUTER JOIN myapp_author ON …followed by oneSELECT ... FROM myapp_tag … WHERE myapp_tag.id IN (…).
Trade‑offs and limitations
While these methods cut query count, they can increase memory usage:
select_relatedwidens the result set; if you join to a table with many columns or large text fields, each row becomes bigger, raising network transfer and memory footprint.prefetch_relatedloads all related objects into memory for each parent instance. Prefetching a heavy many‑to‑many set (e.g., thousands of tags per post) can consume significant RAM.
Practical check: after applying the optimization, monitor memory usage with django‑debug‑toolbar’s panel or by checking len(list(queryset)) and observing the size of the returned objects in a shell.
Actionable closing
Next time you notice a list view slowing down, ask:
- Which relationships are single‑valued? Use
select_related. - Which relationships are multi‑valued? Use
prefetch_related. - Verify the query count with logging or the Debug Toolbar.
- Watch for overly wide rows or large prefetched sets; if they appear, consider fetching only needed fields with
only()or limiting the prefetch withPrefetchobjects.
Applying the right helper turns an O(N) query pattern into a constant‑time fetch, giving your pages a noticeable speed boost without changing your data model.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.