Community Question Bundle
Django ORM N+1 and prefetch_related Drill
Four questions on Django ORM patterns I keep flagging in code review: spotting N+1 in templates, choosing select_related vs prefetch_related, slicing prefetches, and avoiding the .count() trap.
Django ORM N+1 and prefetch_related Drill
Four questions on Django ORM patterns I keep flagging in code review: spotting N+1 in templates, choosing select_related vs prefetch_related, slicing prefetches, and avoiding the .count() trap.
By @freyadiallo
April 16, 2026
·
Updated May 20, 2026
412 views
14
4.4 (9)
A Django template renders {% for order in orders %}{{ order.customer.name }}{% endfor %} and the page hits the DB 41 times for 40 orders. What is the smallest change to the queryset to fix it, and why is it select_related rather than prefetch_related?
Trace it
Trace it with django.db.connection.queries. Order.objects.all() followed by {{ order.customer.name }} in a template across 40 orders runs 41 queries: one for the list, then one per order.customer access. Switch to Order.objects.select_related('customer').all() and you get 1 query with a JOIN.
An author page lists each author with their books. A reviewer suggests select_related('books') and gets FieldError: Invalid field name: books. Why does Django reject this, and what is the correct call?
Trace it
Author.objects.select_related('books')
# raises FieldError: Invalid field name: books -- reverse one-to-many cannot JOIN single-row
Author.objects.prefetch_related('books')
# 2 queries total: SELECT authors + SELECT books WHERE author_id IN (...)
Author.objects.prefetch_related(
Prefetch('books', queryset=Book.objects.filter(year=2024))
)
# still 2 queries, but the second is constrained to 2024 booksA view does qs = Order.objects.filter(status='paid'), then len(qs) and later qs.count(). Both numbers match, but one is cheap and one is expensive. Walk through which is which and explain Django's queryset evaluation rules.
Trace it
Trace it on the DB log:
qs = Order.objects.filter(status='paid')
qs.count() # SELECT COUNT(*) FROM orders WHERE status='paid' -- cheap, single int
len(qs) # SELECT * FROM orders WHERE status='paid' + Python-side len() -- expensive
for o in qs: ... # free, the queryset is already cached from the len() call aboveA list page paginates 25 authors per page and prefetches their books. A reviewer notices the second query loads books for ALL authors, not just the 25 on the current page. Why does this happen and what is the fix?
Trace it
qs = Author.objects.prefetch_related('books').all()
page = Paginator(qs, 25).page(1)
# Surprise: the prefetch runs against the FULL queryset, not the 25-row page
# After the fix:
page = Paginator(Author.objects.order_by('id'), 25).page(1)
ids = [a.pk for a in page.object_list]
authors = Author.objects.filter(pk__in=ids).prefetch_related('books')
# Now prefetch is scoped to the 25 authors on this page only.