If you’ve been working with Django for a while, you’ve probably experienced this.
Your API works perfectly in development.
You test it with a few records.
Everything feels fast.
Then one day production data arrives.
Suddenly an endpoint that used to respond in 300ms now takes 6–10 seconds.
That happened to me.
The Problem
I had an API returning a list of companies along with their owners and projects.
The code looked completely normal.
companies = Company.objects.all()
data = []
for company in companies:
data.append({
"name": company.name,
"owner": company.owner.name,
"projects": [project.name for project in company.projects.all()]
})
Nothing looked suspicious.
Yet the endpoint became painfully slow as the database grew.
What Was Actually Happening?
Every time the loop executed, Django performed additional database queries.
One query fetched the companies.
Another query fetched the owner.
Another query fetched the projects.
If there were 500 companies, Django wasn’t executing 3 queries.
It was executing more than 1,000 queries.
This is known as the N+1 Query Problem.
Many developers unknowingly ship this to production because everything appears fast with a small development database.
How I Found It
The first thing I checked wasn’t the Python code.
It was the SQL queries.
Using Django Debug Toolbar, I discovered the API was generating hundreds of nearly identical SQL statements.
Instead of asking the database once, Django kept asking the same question repeatedly.
The Fix
The solution was surprisingly simple.
Instead of letting Django fetch related objects one by one, I told it to fetch everything in advance.
companies = (
Company.objects
.select_related("owner")
.prefetch_related("projects")
)
Now the rest of the code stayed exactly the same.
No major refactoring.
No caching.
No Redis.
Just better ORM usage.
Why It Works
select_related() performs SQL joins for ForeignKey and OneToOne relationships.
prefetch_related() performs efficient separate queries for Many-to-Many and reverse relationships, then joins the data in Python.
The result?
Instead of executing hundreds of queries, Django executes only a handful.
The Result
Before:
- 1,043 SQL queries
- Response time: 8.4 seconds
After:
- 3 SQL queries
- Response time: 320ms
That’s more than a 20× improvement without changing the database schema or adding any caching.
When Should You Use It?
Use select_related() for:
- ForeignKey
- OneToOneField
Use prefetch_related() for:
- ManyToManyField
- Reverse ForeignKey relationships
Choosing the right method can dramatically reduce database traffic.
Final Thoughts
One of the biggest lessons I’ve learned after years of building Django applications is this:
Performance problems often aren’t caused by slow databases.
They’re caused by asking the database the same question hundreds of times.
Before introducing caching, Redis, Celery, or expensive infrastructure, inspect your SQL queries.
Sometimes the biggest performance improvement comes from changing a single line of Django ORM code.
⸻
Have you ever encountered an N+1 query problem in Django? I’d love to hear how you discovered it and what you did to fix it.
Top comments (0)