Understanding the N+1 Query Problem in Django (Part 1)
Backend EngineeringJul 30, 20265 min read

Understanding the N+1 Query Problem in Django (Part 1)

Learn what the N+1 query problem is in Django, why it slows down your APIs, and how to eliminate unnecessary database queries using `select_related()` and `prefetch_related()`. This guide covers the fundamentals of Django ORM optimization with practical examples and best practices for building faster, more scalable applications.


Part 1 of the API Optimization Series

One of the easiest ways to slow down a Django application isn't writing complex business logic—it's accidentally making your database do far more work than necessary.

If you've ever built an API with Django REST Framework and noticed an endpoint becoming slower as your data grows, there's a good chance you've encountered the N+1 query problem.

In this article, we'll break down what it is, why it happens, and how Django's select_related() and prefetch_related() help eliminate it.


What Is the N+1 Query Problem?

Imagine you have two simple models:

class Author(models.Model):
    name = models.CharField(max_length=100)
 
 
class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE
    )

Suppose your API returns a list of books together with their authors.

[
    {
        "title": "Django for APIs",
        "author": "John"
    },
    {
        "title": "REST Mastery",
        "author": "John"
    },
    {
        "title": "Python Tips",
        "author": "Alice"
    }
]

A straightforward queryset might look like this:

books = Book.objects.all()

At first glance, this seems perfectly fine.

However, here's what actually happens behind the scenes.

The first query retrieves every book:

SELECT * FROM books;

Then, as Django serializes each book, it fetches the related author individually.

SELECT * FROM authors WHERE id = 1;
SELECT * FROM authors WHERE id = 1;
SELECT * FROM authors WHERE id = 2;
...

If there are 100 books, Django performs:

  • 1 query to retrieve the books.
  • 100 additional queries to retrieve their authors.

That's 101 database queries for a single request.

This is exactly why it's called the N+1 query problem.

  • 1 = the initial query.
  • N = one additional query for every object returned.

Why Is It a Problem?

Databases are optimized to process data efficiently.

The expensive part is often the repeated communication between your application and the database, not necessarily the query itself.

Imagine every query takes only 3 ms.

With the N+1 problem:

101 × 3 ms ≈ 303 ms

If the same data can be fetched in only 2 queries:

2 × 3 ms ≈ 6 ms

As your application grows, the performance difference becomes significant.

Number of BooksQueries Executed
1011
100101
1,0001,001
10,00010,001

The number of queries grows linearly with your dataset, causing API response times to degrade.


Eliminating N+1 with select_related()

For ForeignKey and OneToOneField relationships, Django provides select_related().

Instead of loading the related author one book at a time, Django performs a SQL JOIN.

books = Book.objects.select_related("author")

The generated SQL resembles:

SELECT
    books.*,
    authors.*
FROM books
JOIN authors
ON books.author_id = authors.id;

Now Django retrieves both books and their authors in a single query.

Instead of:

Books

Author

Author

Author

You get:

Books + Authors

One Query

When Should You Use select_related()?

select_related() is designed for relationships where each object references a single related object.

Use it with:

  • ForeignKey
  • OneToOneField

Example:

class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)

Instead of:

Order.objects.all()

Use:

Order.objects.select_related("customer")

What About Many-to-Many Relationships?

select_related() doesn't work for ManyToManyField or reverse relationships because SQL joins would duplicate rows and dramatically increase the size of the result set.

Instead, Django provides prefetch_related().

courses = Course.objects.prefetch_related("students")

Rather than performing a huge SQL join, Django executes two efficient queries:

  1. Retrieve all courses.
  2. Retrieve all related students using a WHERE ... IN (...) query.

It then connects the data in memory, avoiding unnecessary database round trips.


select_related() vs prefetch_related()

Relationship TypeRecommended Method
ForeignKeyselect_related()
OneToOneFieldselect_related()
ManyToManyFieldprefetch_related()
Reverse ForeignKeyprefetch_related()

Choosing the correct method can reduce hundreds—or even thousands—of unnecessary database queries.


A Real Django REST Framework Example

Suppose your API returns blog posts together with their author and comments.

queryset = (
    Post.objects
    .select_related("author")
    .prefetch_related("comments")
)

Here:

  • author is a ForeignKey, so select_related() is appropriate.
  • comments is a reverse relationship, so prefetch_related() prevents another N+1 query problem.

A small change like this can dramatically improve API performance as your application scales.


How to Detect N+1 Queries

Even experienced developers introduce N+1 queries without realizing it. Fortunately, Django provides several ways to detect them during development.

1. Django Debug Toolbar

The Django Debug Toolbar displays every SQL query executed during a request, making it one of the easiest ways to identify repeated queries.

Look for patterns such as:

SELECT * FROM authors WHERE id = 1;
SELECT * FROM authors WHERE id = 2;
SELECT * FROM authors WHERE id = 3;
...

If you see dozens or hundreds of nearly identical queries, you've likely found an N+1 problem.

2. Enable SQL Logging

Configure Django to log SQL queries during development. Reviewing the logs often reveals repeated database hits that are otherwise hidden behind your serializers.

3. Write Query Count Tests

Django's assertNumQueries() allows you to ensure your views don't accidentally introduce additional queries over time.

with self.assertNumQueries(2):
    response = self.client.get("/api/books/")

These tests are especially valuable for large teams where future changes might unintentionally reintroduce performance issues.


Best Practices

As a general rule:

  • Use select_related() for ForeignKey and OneToOneField relationships.
  • Use prefetch_related() for ManyToManyField and reverse relationships.
  • Always profile your queries before optimizing.
  • Don't assume an endpoint is efficient simply because it works correctly.
  • Keep an eye on query counts as your serializers become more complex.

Final Thoughts

The N+1 query problem is one of the most common performance issues in Django applications, yet it's also one of the easiest to fix once you understand how Django loads related objects.

A single call to select_related() or prefetch_related() can reduce hundreds—or even thousands—of unnecessary database queries, leading to significantly faster APIs and better scalability.

Performance optimization isn't always about writing more code. Often, it's about helping Django retrieve data more efficiently.

In Part 2, we'll explore more advanced ORM optimization techniques, including:

  • Nested prefetch_related()
  • The Prefetch object
  • Filtered prefetches
  • Combining select_related() and prefetch_related()
  • Memory trade-offs
  • Profiling complex query performance

Further Reading

If you'd like to dive deeper into Django's ORM and query optimization, the following official resources are excellent references:


API Optimization Series

This article is part of an ongoing series exploring practical techniques for building high-performance Django APIs.

  • ✅ Part 1: Understanding the N+1 Query Problem in Django (This Article)
  • 🚧 Part 2: Advanced Query Optimization with Prefetch
  • 🚧 Part 3: Database Indexing Strategies Every Django Developer Should Know
  • 🚧 Part 4: Cursor Pagination vs. Offset Pagination
  • 🚧 Part 5: Profiling and Debugging Slow Django APIs
  • 🚧 Part 6: Caching Strategies for Faster API Responses
  • 🚧 Part 7: Connection Pooling with PgBouncer
  • 🚧 Part 8: Denormalization & Materialized Views for Read-Heavy Workloads

Stay tuned as we continue exploring the techniques used to build scalable, production-ready Django applications.

More posts

Backend EngineeringJul 30, 20263 min read

Before You Scale: A Production Readiness Series

A comprehensive 20-part software engineering series covering production readiness, performance optimization, database tuning, API design, caching, Docker, CI/CD, monitoring, security, load testing, horizontal scaling, high availability, disaster recovery, and cost optimization. Learn how to build applications that are reliable, scalable, and ready for real-world traffic.