Lesson 16 / 26
Avoiding N+1 Queries
select_related and prefetch_related.
One query per row is a trap
Accessing a related object in a loop (p.category.name) runs one extra query per row: the N+1 problem, often invisible in development and slow in production. select_related joins single-valued relations (ForeignKey, OneToOne) into the same query; prefetch_related fetches multi-valued relations in one extra query. Measure with assertNumQueries in tests, Django Debug Toolbar, or query logging.
Counting queries with and without select_related, run
I ran this script with python manage.py shell --no-imports in the demo project (Django 6.1.1, SQLite). Listing four products with their category names takes 5 queries without select_related (1 for products plus 4 for categories) and 1 query with it.
from django.db import connection
from django.test.utils import CaptureQueriesContext
from catalog.models import Product
with CaptureQueriesContext(connection) as naive:
names = [f"{p.name}/{p.category.name}" for p in Product.objects.all()] # one query per category access
with CaptureQueriesContext(connection) as joined:
names2 = [f"{p.name}/{p.category.name}" for p in Product.objects.select_related("category")]
print(names)
print("without select_related:", len(naive.captured_queries), "queries")
print("with select_related :", len(joined.captured_queries), "query")
Output:
['Lamp/Home', 'Mug/Home', 'Notebook/Office', 'Pen/Office'] without select_related: 5 queries with select_related : 1 query
Lock query counts in tests
assertNumQueries in a view test fails the build if someone later introduces an N+1 query.
Quick check: Which method fixes N+1 queries for a ForeignKey?
- order_by
- prefetch_related only for strings
- select_related
- distinct
Answer
select_related — Join single-valued relations.