Query Performance: select_related & prefetch_related

The N+1 query problem is when fetching a list of N objects with one query, then touching a related object on each one in a loop, makes Django run one extra query per row — 1 + N queries instead of one.

Learn Query Performance: select_related & prefetch_related in our free Django course — a beginner-friendly interactive lesson with worked examples, a…

Part of the free Django course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

In this lesson you'll see how N+1 sneaks in, how select_related collapses it into a single JOIN, how prefetch_related turns it into just two queries, and how to trim columns with only()/defer() and actually count the queries you run.

Imagine a page that lists blog posts with each author's name. You write the obvious loop: fetch the posts, then read post.author.name for each one. The list itself is a single query, but post.author was never loaded, so Django quietly fires one extra query per post to fetch the author. With 100 posts that is 1 + 100 = 101 queries — the classic N+1 .

The query log makes the damage obvious — the same author SELECT repeats over and over:

Django gives you two tools to load related data up front. select_related follows to-one relations ( ForeignKey , OneToOneField ) with a SQL JOIN , so the author comes back inside the same row as the post — the N+1 becomes 1 query . prefetch_related handles to-many relations ( ManyToManyField , reverse ForeignKey ) by running a second query for all the related rows and joining them to their parents in Python — N+1 becomes 2 queries .

You can combine and chain them, and even reach across multiple hops with double-underscore syntax:

Once you have the right number of queries, you can shrink how much each one returns. only() loads just the columns you name (plus the primary key); defer() loads everything except the columns you name. Both are useful when a table has heavy TextField or binary columns you do not need on a list page.

To prove your optimisation worked, count the queries. In development read connection.queries ; in tests use assertNumQueries :

Fill in the blank so the optimised version loads the authors with the posts and ends up at a single query, not 1 + N.

❌ Using select_related on a ManyToMany or reverse FK

Django raises FieldError: Invalid field name(s) given in select_related — those relations return many rows, so a single JOIN can't represent them.

✅ Fix: use prefetch_related("tags") for to-many relations and reserve select_related for ForeignKey/OneToOne.

❌ The N+1 is still there after adding select_related

You touched a relation you didn't name, e.g. post.author was loaded but you used post.category.name .

✅ Fix: name every relation you access — select_related("author", "category") — or count queries to find what you missed.

You excluded a column with only() or defer() , then accessed it in a loop, so Django re-fetches it per row.

✅ Fix: include every column you actually use in only(...) , or drop only() for that view.

Given a queryset's optimisations, compute how many queries it runs. Each select_related field folds into the base query (a JOIN), while each prefetch_related field adds one separate query.

Lesson complete — you can find and fix N+1 queries!

You can now recognise the 1 + N pattern, reach for select_related on to-one relations and prefetch_related on to-many relations, trim columns with only() and defer() , and prove the win by counting queries with connection.queries and assertNumQueries .

🚀 Up next: Custom Managers & QuerySets — package these optimised queries into reusable, named methods.

Practice quiz

What causes the N+1 query problem?

  • Accessing an unloaded related object on each row in a loop
  • Using too many filter() calls
  • Calling count() once
  • Indexing the database

Answer: Accessing an unloaded related object on each row in a loop. Touching a related object that was not loaded up front runs one extra query per row.

For a list of N objects, how many queries does the naive N+1 pattern run?

  • N
  • 1 + N
  • 2
  • N times N

Answer: 1 + N. One query loads the list, then one extra query per row: 1 + N total.

Which method should you use for a ForeignKey or OneToOne relation?

  • prefetch_related
  • only
  • select_related
  • defer

Answer: select_related. select_related follows to-one relations with a SQL JOIN, fetching them in one query.

How does select_related fetch the related data?

  • With a second query
  • In Python
  • Not at all
  • With a SQL JOIN

Answer: With a SQL JOIN. select_related adds a JOIN so the related row comes back inside the same query.

Which method should you use for a ManyToMany or reverse ForeignKey relation?

  • prefetch_related
  • select_related
  • only
  • values

Answer: prefetch_related. prefetch_related handles to-many relations with a separate query joined in Python.

How many queries does prefetch_related turn an N+1 into?

  • 1
  • 2
  • N
  • 0

Answer: 2. prefetch_related runs the main query plus one extra query for the related rows: 2 total.

What does only('title') do?

  • Loads every column
  • Deletes other columns
  • Loads only the named columns plus the primary key
  • Orders by title

Answer: Loads only the named columns plus the primary key. only() loads just the named columns (plus the primary key).

What does defer('body') do?

  • Loads only body
  • Renames body
  • Skips the whole row
  • Loads every column except body

Answer: Loads every column except body. defer() loads everything except the named columns.

What happens if you touch a column you excluded with only() inside a loop?

  • Django re-fetches it, causing an extra query per row
  • It raises a SyntaxError
  • Nothing, it is cached
  • The column is silently None

Answer: Django re-fetches it, causing an extra query per row. Touching a deferred column triggers a lazy query per row, reintroducing N+1.

Which test helper fails unless an exact number of queries runs?

  • assertEqual
  • assertNumQueries
  • assertQuerysetEqual
  • assertContains

Answer: assertNumQueries. assertNumQueries(n) fails if the wrapped code does not run exactly n queries.