Django from Basics to Production · Django Fundamentals
Relationships and the N+1 Query Problem
Model foreign keys and many-to-many relations, and use select_related and prefetch_related to avoid one query per row.
What you'll build
Authors, books and tags, a listing that prints each book with its author and tags, and a query counter showing the same listing taking 1 + N + N queries naively and 2 with select_related and prefetch_related.
The idea
ForeignKeycreates a many-to-one relation;ManyToManyFielda many-to-many one through a join table.- Accessing
book.authorloads the author on demand: in a loop over 50 books, that's 50 extra queries (N+1). select_related("author")joins the related table in the same query (for ForeignKey and OneToOne).prefetch_related("tags")runs one extra query for all related rows and joins them in Python (for many-to-many and reverse relations).
The code
import sys, types
import django
from django.conf import settings
sys.modules["shop"] = types.ModuleType("shop") # lets this single file act as the "shop" app
sys.modules["shop"].__file__ = __file__
settings.configure(
SECRET_KEY="dev-only", DEBUG=False, ALLOWED_HOSTS=["testserver"], ROOT_URLCONF=__name__,
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
INSTALLED_APPS=["django.contrib.contenttypes", "django.contrib.auth", "shop"],
)
django.setup()
from django.db import connection, models
from django.test.utils import CaptureQueriesContext
class Author(models.Model):
name = models.CharField(max_length=100)
class Meta: app_label = "shop"
class Tag(models.Model):
name = models.CharField(max_length=30)
class Meta: app_label = "shop"
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
tags = models.ManyToManyField(Tag)
class Meta: app_label = "shop"
with connection.schema_editor() as editor:
for model in (Author, Tag, Book):
editor.create_model(model)
classic, scifi = Tag.objects.create(name="classic"), Tag.objects.create(name="sci-fi")
for i in range(5):
author = Author.objects.create(name=f"Author {i}")
Book.objects.create(title=f"Book {i}", author=author).tags.add(classic if i % 2 else scifi)
def listing(qs):
return [f"{b.title} by {b.author.name} [{', '.join(t.name for t in b.tags.all())}]" for b in qs]
with CaptureQueriesContext(connection) as naive:
rows = listing(Book.objects.all())
with CaptureQueriesContext(connection) as tuned:
rows2 = listing(Book.objects.select_related("author").prefetch_related("tags"))
print(rows[0])
print("same output:", rows == rows2)
print("queries naive:", len(naive), "| with select_related + prefetch_related:", len(tuned))
Output
Book 0 by Author 0 [sci-fi] same output: True queries naive: 11 | with select_related + prefetch_related: 2
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Templates and serializers that access related objects in loops: the N+1 is invisible in the code.
- Using
select_relatedfor many-to-many (it doesn't work there; useprefetch_related). - Fetching whole related objects when only one field is needed; consider
values()oronly().
Key points
- Accessing relations in loops causes N+1 queries.
select_relatedjoins foreign keys;prefetch_relatedbatches many-to-many and reverse relations.- Measure query counts in tests (
assertNumQueries) to catch regressions.
Further reading: select_related · prefetch_related · Database access optimization
Practice questions
What is the N+1 query problem?
Answer
Loading a list with one query and then one extra query per item to fetch a related object, so N items cost N+1 queries.
When do you use select_related versus prefetch_related?
Answer
select_related joins single-valued relations (ForeignKey, OneToOne) in the same query; prefetch_related runs a separate query for multi-valued relations (many-to-many, reverse foreign keys).
How can tests catch N+1 regressions?
Answer
With assertNumQueries (or CaptureQueriesContext) around a view or serializer call, failing when the number of queries grows.
Open in the app to mark lessons complete, take notes and track your progress.