Eager vs Lazy Loading — When to Use Each
debt(d5/e3/b5/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints.tools list includes laravel-debugbar, doctrine-profiler, and clockwork — all specialist profiling/debugging tools rather than default linters or compilers. The code_pattern notes '50+ identical queries' visible in these tools, meaning the problem is silent in normal development without these tools enabled, but detectable with them. Not quite d7 because these tools are relatively accessible in PHP ORMs.
Closest to 'simple parameterised fix' (e3). The quick_fix confirms switching to eager loading with `with()` for relations accessed in loops — a targeted change within one component or query layer. It's not a one-liner across the board (e1) because you must identify all affected queries and potentially adjust selects, but it doesn't span multiple architectural layers.
Closest to 'persistent productivity tax' (b5). The term applies_to all contexts (web, cli, queue-worker) and the common_mistakes note 'ORM lazy loading enabled globally' causes hidden N+1 in every list view. This means a poor default choice imposes an ongoing tax across many features and work streams, but it doesn't redefine the system's shape — it's a recurring pattern decision rather than an architectural constraint.
Closest to 'serious trap' (t7). The misconception field explicitly states 'Always prefer eager loading to avoid N+1 queries' — a plausible, well-intentioned rule that is actively wrong in many real cases (over-fetching unused relations). This contradicts the common advice given to developers learning about N+1, making it a serious trap: the 'obvious fix' (always eager load) is itself a source of a different performance problem.
Also Known As
TL;DR
Explanation
Eager loading (JOIN FETCH in Doctrine, with() in Eloquent) retrieves related entities in the same query as the parent — optimal when you know you'll use the association. Lazy loading defers fetching until the property is accessed for the first time — convenient for occasionally-needed associations, but silently causes N+1 when accessed in a loop. Doctrine defaults to LAZY; Eloquent defaults to lazy (without with()). Decision rules: if you always display the association on a list page — eager load it. If the association is rarely used (only on detail pages) — lazy is acceptable with careful monitoring. Avoid lazy loading entirely in performance-critical loops; use batch loading (IN query) as a middle ground between one-per-row and one-JOIN-for-all.
Common Misconception
Why It Matters
Common Mistakes
- ORM lazy loading enabled globally — hidden N+1 queries in every list view with no warning.
- Not measuring actual query counts — assuming the ORM is efficient without checking.
- Blanket eager loading all relationships on every query — loads gigabytes of related data for pages that show one field.
- Not using select() to limit columns when eager loading — fetches full rows when only IDs or names are needed.
Code Examples
// Lazy default — N+1 problem
$posts = Post::all(); // 1 query
foreach ($posts as $post) {
echo $post->author->name; // N queries
}
// Eager load when you know you'll need the relation
$posts = Post::with('author')->get(); // 2 queries, no matter how many posts
// Lazy eager load — load relation for an already-fetched collection
$posts->load('tags'); // 1 additional query for all tags
// Conditional eager loading
$posts = Post::when($includeComments, fn($q) => $q->with('comments'))->get();
// Laravel Debugbar or Telescope shows query count — aim for < 5 per request