The N+1 problem is one of those things that’s easy to miss, silently kills your application’s performance at scale, and takes five minutes to fix once you know what to look for.
What is it?
It happens when your code runs one query to fetch a list, then runs an additional query for each item in that list to fetch related data.
Say you have Cars and each car has Wheels. The naive approach:
-- 1 query to get all cars
SELECT * FROM cars;
-- Then for every car (N cars = N queries)
SELECT * FROM wheels WHERE car_id = 1;
SELECT * FROM wheels WHERE car_id = 2;
SELECT * FROM wheels WHERE car_id = 3;
-- ... and so on
If you have 100 cars, that’s 101 queries. For 1000 cars, 1001 queries. This compounds fast.
Why it’s easy to miss
Because it doesn’t look broken. The data comes back correctly. Your tests pass. It only reveals itself when your dataset grows and you start wondering why a simple page takes 3 seconds to load.
Fixing it in Django
Django’s ORM defaults to lazy loading — it fetches related objects only when you access them, which triggers the N+1 pattern automatically.
select_related() — for ForeignKey and OneToOne relationships. Joins the tables in a single query:
# Bad — triggers N+1
cars = Car.objects.all()
for car in cars:
print(car.engine.type) # new query per car
# Good — one query with a JOIN
cars = Car.objects.select_related('engine').all()
prefetch_related() — for ManyToMany and reverse ForeignKey. Runs two queries total, then joins in Python:
# Good — 2 queries total instead of N+1
cars = Car.objects.prefetch_related('wheels').all()
Debugging tools worth knowing:
- django-debug-toolbar — shows you every query on the page
- nplusone — raises errors when N+1 is detected
- Scout APM — for production monitoring
Fixing it in Laravel
Eloquent has the same lazy-loading default, same problem.
with() — eager loads relationships upfront:
// Bad — 11 queries for 10 cars
$cars = Car::all();
foreach ($cars as $car) {
echo $car->wheels; // new query
}
// Good — 2 queries total
$cars = Car::with('wheels')->get();
load() — when you already have a collection and want to load relationships on it:
$cars = Car::all();
$cars->load('wheels');
Globally prevent lazy loading (useful in development):
// In AppServiceProvider::boot()
Model::preventLazyLoading(! app()->isProduction());
This throws an exception whenever lazy loading is triggered, forcing you to fix it before it reaches production.
The habit to build
Whenever you’re looping over a collection and accessing a relationship inside the loop — stop. That’s almost always N+1. Reach for select_related, prefetch_related, or with() instead.
It’s a small habit that keeps your queries predictable no matter how large your data grows.