The N+1 Query Problem, and Four Ways to Catch It
Why the most common ORM performance bug hides in development and only appears under real data.
An N+1 is one query to fetch a list, then one more per row to fetch a relation. With ten seeded rows it is invisible. With ten thousand it is an outage.
What it looks like#
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // one query per post
}The fix#
$posts = Post::query()->with('author')->get();Catching it automatically#
Four approaches, in increasing order of usefulness:
- Eyeballing the debug bar. Works, but only when someone looks.
- Strict mode.
Model::preventLazyLoading()in a non-production environment throws the moment a relation loads lazily. - Query-count assertions in tests. Assert that an index page issues a bounded number of queries. The test fails when someone adds a lazy relation later.
- Production query logging with a threshold. Catches the ones that only appear with real data shapes.
Why strict mode alone is not enough#
It only fires on code paths you actually execute. A rarely visited page can still ship an N+1 for months. Pair it with query-count assertions on every list endpoint.