Back to blog
2 min read

Killing N+1 Queries in Django

  • Django
  • Python
  • Performance

N+1 queries are the performance bug that ships to production unnoticed. The page works, the tests pass, and then the database struggles under real traffic. Let me walk through what causes it and how Django's ORM lets you fix it cleanly.

What an N+1 query looks like

Say you have Book rows, each with a foreign key to Author:

for book in Book.objects.all():
    print(book.title, book.author.name)

This runs one query to fetch the books, then one more query per book to fetch its author. A hundred books means a hundred and one queries. That is the N+1: one query, plus N follow-ups.

The ORM does not warn you. Each book.author access looks like a plain attribute lookup, but it silently hits the database.

Spotting it

Two habits catch almost every case:

  1. Install django-debug-toolbar in development. It shows the query count and flags duplicated queries on every page.
  2. In tests, assert the query count around code you care about:
with self.assertNumQueries(2):
    render_book_list()

If a refactor turns 2 into 50, the test fails before the code reaches review.

For a ForeignKey or OneToOneField, select_related pulls the related row in with a SQL join:

for book in Book.objects.select_related("author"):
    print(book.title, book.author.name)

That is now a single query. You can follow chains too, such as select_related("author__publisher").

A join cannot fan out across a many-to-many or reverse foreign key without multiplying rows. prefetch_related instead runs a second query and stitches the results together in Python:

for author in Author.objects.prefetch_related("books"):
    for book in author.books.all():
        print(book.title)

Two queries total, no matter how many authors.

Prefetch objects for control

When you need a filtered or ordered prefetch, pass a Prefetch object:

from django.db.models import Prefetch

recent = Prefetch(
    "books",
    queryset=Book.objects.filter(published__year=2026),
)
Author.objects.prefetch_related(recent)

This is also how you avoid pulling in huge relations you only partly need.

Push work into the database

Once the query count is under control, push aggregation down too. Counting related rows in Python is another quiet N+1:

from django.db.models import Count

Author.objects.annotate(book_count=Count("books"))

The database returns the count directly. only() and defer() go further, trimming columns you will not read.

The mental model

Every time you cross a relation inside a loop, ask one question: did I tell the ORM I would need this? If the answer is no, you have an N+1 waiting to happen. Use select_related for forward relations, prefetch_related for everything else, and a query-count test to keep it honest.