N+1 Problem in Doctrine & Eloquent
debt(d7/e3/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The term's detection_hints list specialist tools (laravel-debugbar, laravel-strict-mode, doctrine-profiler, clockwork), but these require deliberate setup — they are not default linters. The common_mistakes field explicitly notes 'N+1 queries are silent without [query logging]', meaning in production or without tooling, the problem is invisible. Strict mode can surface it at runtime in development, but it is not on by default, placing it between d5 and d9; d7 best fits because it requires non-default tooling or careful review.
Closest to 'simple parameterised fix (replace pattern with safer alternative)' (e3). The quick_fix describes enabling strict mode or adding with() for eager loading — a small, targeted change. The common_mistakes note that with() must be applied correctly (e.g. not missing nested relations), so it's slightly more than a one-liner but well within a single-component fix. e3 is the right anchor.
Closest to 'persistent productivity tax' (b5). The applies_to scope covers both web and cli contexts in PHP, meaning any code that queries collections with relationships is potentially affected. The problem recurs across many features and requires ongoing vigilance (every new relation access is a potential re-introduction), creating a persistent productivity tax. It doesn't define the system's shape, but it slows many work streams, fitting b5.
Closest to 'serious trap (contradicts how a similar concept works elsewhere)' (t7). The misconception field states explicitly: 'ORMs automatically optimise relationship loading' — developers coming from other patterns or from reading ORM documentation that emphasises convenience naturally assume the ORM handles this. The trap contradicts the intuition that high-level abstractions optimise low-level details. The 'obvious' way (access the relation in a loop) is exactly the wrong way, aligning with t7.
Also Known As
TL;DR
Explanation
The N+1 problem: fetching N orders then calling $order->customer inside a loop fires N additional queries — one per order — instead of a single JOIN. In Doctrine: use DQL JOIN FETCH or addSelect on a QueryBuilder to eager-load associations. In Eloquent: use with('customer') on the query. Detect N+1 in development with Laravel Telescope, Debugbar, or Clockwork (all show per-request query counts). In Doctrine enable SQL logging to a file. A page making 300 queries that should make 3 is the signature. Fix: identify the loop, move the JOIN upstream, verify query count drops. For deeply nested associations, consider a dedicated read-model query bypassing the ORM entirely.
Common Misconception
Why It Matters
Common Mistakes
- Not enabling ORM query logging in development — N+1 queries are silent without it.
- Lazy-loaded relationships in loops — the classic ORM N+1 pattern.
- Using with() for eager loading but then accessing a non-eager-loaded relationship inside the loop.
- Not using tools like Laravel Debugbar or Symfony Profiler to detect query counts per request.
Code Examples
$orders = $em->findAll(Order::class); // 1 query
foreach ($orders as $o) {
echo $o->getCustomer()->getName(); // N queries!
}
$orders = $em->createQuery(
'SELECT o, c FROM Order o JOIN FETCH o.customer c'
)->getResult(); // 1 query