← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

N+1 Query Problem

Performance PHP 5.0+ Intermediate
debt(d7/e3/b5/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7), slightly better. Tools like Laravel Debugbar and Clockwork (listed in detection_hints.tools) can surface N+1 at runtime by showing 50+ identical queries, and phpstan with plugins can flag it statically — but these require deliberate installation and configuration, and the problem is completely invisible without a query debugger as noted in common_mistakes. It won't surface in normal test runs unless query counting is explicitly asserted.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3). The quick_fix is adding with() or equivalent eager loading to the outer query — a small, targeted change. However, common_mistakes note it can spread to controllers, API resources, and view composers, meaning the same pattern may need fixing in several places. This pushes it slightly above e1 but stays at e3 because each individual fix is a small local change rather than a cross-cutting refactor.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5). The problem applies across web, cli, and queue-worker contexts, meaning it can silently degrade any feature that iterates over a collection with relationships. Without a query debugger permanently installed, every new feature touching relational data risks reintroducing it. It doesn't restructure the entire codebase (not b7) but it does impose an ongoing vigilance tax across many work streams.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception field explicitly states developers believe N+1 only affects ORMs, when in reality any loop-with-query pattern causes it. This contradicts intuition: developers who carefully write manual SQL can still introduce it, and adding with() to the outer query but missing nested relationships (with('orders') vs with('orders.items')) is a subtle secondary trap. The 'obvious' fix (just add with()) is incomplete without understanding relationship depth.

About DEBT scoring →

Also Known As

N+1 query problem N+1 queries lazy load N+1

TL;DR

Executing one query to get N records, then N more queries to fetch related data — one per record.

Explanation

The N+1 problem occurs when code fetches a list of entities and then executes an additional query for each one — typically to load a related record. For 100 rows this means 101 queries; for 1000 rows, 1001. Database round-trip overhead makes this disproportionately slow compared to a single JOIN or a single IN() query. The fix is to pre-load all related data in one query and look it up from a keyed array in memory.

Diagram

sequenceDiagram
    participant APP as Application
    participant DB as Database
    APP->>DB: SELECT * FROM posts - 1 query
    DB-->>APP: 100 posts returned
    loop For each of 100 posts
        APP->>DB: SELECT * FROM users WHERE id = ?
        DB-->>APP: 1 user
    end
    Note over APP,DB: 1 + 100 = 101 queries total
    Note over APP,DB: Fix: eager load with JOIN<br/>SELECT * FROM users WHERE id IN (1,2,3...)

Common Misconception

The N+1 problem only affects ORMs. Any code that queries inside a loop — fetching a user's avatar for each post in a list — produces N+1 queries. ORMs make it easier to introduce accidentally; the underlying problem is a missing JOIN or eager load regardless of abstraction.

Why It Matters

N+1 queries are the most common reason a page that looks fine locally becomes unusable under real data — 100 rows means 101 queries, 1000 rows means 1001. Eager loading eliminates this with a single extra JOIN or IN query.

Common Mistakes

  • Accessing a relationship inside a loop without eager loading — ORM lazy-loads on every iteration.
  • Adding with() to the outer query but forgetting nested relationships (with('orders.items') instead of with('orders')).
  • Fixing N+1 in controllers but leaving it in API resources or view composers that loop over collections.
  • Not installing a query debugger (Laravel Debugbar, DBAL logger) — N+1 is invisible without query counting.

Code Examples

✗ Vulnerable
// N+1: 1 query for posts + N queries for author names
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->author->name; // triggers a new query per post
}
✓ Fixed
$posts = Post::with('author')->get(); // 2 queries total
foreach ($posts as $post) {
    echo $post->author->name;
}

Added 13 Mar 2026
Edited 22 Mar 2026
Views 94
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 1 ping T 0 pings F 0 pings S 1 ping S 0 pings M 0 pings T 0 pings W 1 ping T 6 pings F 6 pings S 10 pings S 7 pings M 0 pings T 0 pings W 1 ping T 0 pings F 0 pings S 0 pings S 1 ping M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 1 ping S 0 pings M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Scrapy 29 Amazonbot 9 Perplexity 9 Google 5 Ahrefs 5 SEMrush 4 Bing 3 Unknown AI 2 ChatGPT 2 Claude 1 Meta AI 1 PetalBot 1
crawler 68 crawler_json 3
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: Low
⚡ Quick Fix
Use eager loading (with() in Laravel, addSelect/join in Doctrine) to fetch related data in one query instead of N+1
📦 Applies To
PHP 5.0+ web cli queue-worker laravel doctrine eloquent
🔗 Prerequisites
🔍 Detection Hints
Loop over collection calling ->relation or ->find() per iteration; Laravel Debugbar shows 50+ identical queries
Auto-detectable: ✓ Yes laravel-debugbar clockwork phpstan
⚠ Related Problems
🤖 AI Agent
Confidence: High False Positives: Medium ✗ Manual fix Fix: Medium Context: File Tests: Update


✓ schema.org compliant