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:

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.