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

N+1 Query Problem

Database PHP 5.1+ Intermediate
debt(d7/e3/b5/t5)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). The detection_hints field explicitly states automated detection is 'no', and the code pattern (PDO prepare/execute inside foreach) requires a human reviewer or runtime query-count profiler to spot. No standard linter from the term's tooling catches this automatically; it only surfaces under real data loads or deliberate review.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3). The quick_fix states replacing per-loop queries with a single JOIN or WHERE id IN (...) — this is a targeted rewrite within one query/data-access block. It's slightly more than a one-liner (requires restructuring the fetch and the loop) but stays within a single component, matching e3.

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

Closest to 'persistent productivity tax' (b5). The term applies to both web and CLI contexts across PHP 5.1+, meaning it can appear anywhere database access touches looped result sets. It doesn't define the entire system architecture, but undetected N+1 patterns spread across data-access layers and slow down many work streams — every feature involving related data is at risk.

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

Closest to 'notable trap — a documented gotcha most devs eventually learn' (t5). The misconception is explicit: developers believe N+1 only matters with large datasets, but even 20 records incur 20 round-trips. This is a well-documented gotcha that surprises intermediate developers but is eventually learned — fitting t5 precisely.

About DEBT scoring →

Also Known As

N+1 problem lazy loading N+1 query loop anti-pattern

TL;DR

A performance anti-pattern where fetching N records triggers N additional queries — one per record — instead of a single JOIN.

Explanation

Classic example: fetch 100 users (1 query), then loop and fetch each user's orders separately (100 queries) = 101 queries total. The fix is eager loading — a single JOIN or a second query with WHERE user_id IN (...). ORMs like Eloquent and Doctrine make N+1 easy to introduce through lazy-loading relationships. SQL query logging or a profiler (Blackfire, Laravel Debugbar) reveals N+1 patterns.

Diagram

sequenceDiagram
    participant PHP
    participant MySQL
    PHP->>MySQL: SELECT * FROM users (1 query)
    loop For each of N users
        PHP->>MySQL: SELECT * FROM orders WHERE user_id=?
    end
    Note over PHP,MySQL: N+1 queries total<br/>Fix: JOIN or WHERE IN

Common Misconception

N+1 only matters with large datasets. Even with 20 records, N+1 adds 20 round-trips to the database — at 1ms per round-trip that's 20ms of pure overhead before any processing.

Why It Matters

N+1 queries turn a page that should take 5ms into one that takes 500ms under real data volumes. It's one of the most common performance bugs in web applications.

Common Mistakes

  • Calling a database function inside a foreach loop over query results.
  • Using ORM lazy-loading relationships without checking the query count in development.
  • Fixing N+1 with N separate queries instead of a single IN clause.

Avoid When

  • Never call a database query inside a foreach loop over query results without batching.

When To Use

  • Use JOIN or WHERE id IN (...) to batch-load related records in one query.
  • Use eager loading in ORMs (with(), load()) to prevent automatic lazy-loading N+1.

Code Examples

✗ Vulnerable
// N+1: 1 query for users + N queries for orders
$users = $pdo->query('SELECT * FROM users')->fetchAll();
foreach ($users as $user) {
    $stmt = $pdo->prepare('SELECT * FROM orders WHERE user_id = ?');
    $stmt->execute([$user['id']]);
    $user['orders'] = $stmt->fetchAll(); // N separate queries!
}
✓ Fixed
// Fix: one query with JOIN or IN clause
$userIds = array_column($users, 'id');
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
$stmt = $pdo->prepare("SELECT * FROM orders WHERE user_id IN ($placeholders)");
$stmt->execute($userIds);
$orders = $stmt->fetchAll();

// Group by user_id in PHP
$ordersByUser = [];
foreach ($orders as $order) {
    $ordersByUser[$order['user_id']][] = $order;
}

Added 31 Mar 2026
Edited 5 Apr 2026
Views 44
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 1 ping W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 1 ping T 0 pings F 4 pings S 1 ping S 2 pings M 0 pings T 0 pings W 0 pings T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 2 pings W 0 pings T 0 pings F 0 pings S 1 ping S 0 pings M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Perplexity 6 Google 5 Scrapy 5 Ahrefs 3 SEMrush 3 Meta AI 2 ChatGPT 1 Unknown AI 1 Claude 1 Majestic 1 PetalBot 1
crawler 27 crawler_json 2
DEV INTEL Tools & Severity
⚙ Fix effort: Medium
⚡ Quick Fix
Use a JOIN or WHERE id IN (...) to batch-load related records in one query instead of one query per record
📦 Applies To
PHP 5.1+ web cli
🔗 Prerequisites
🔍 Detection Hints
PDO prepare/execute inside foreach over query results
Auto-detectable: ✗ No
⚠ Related Problems
🤖 AI Agent
Confidence: High False Positives: Low ✗ Manual fix Fix: Medium Context: Function Tests: Update


✓ schema.org compliant