0

Object pooling and reuse patterns

Advanced5 min read·php-15-007
performancecompare

Concept

The N+1 query problem is one of the most common and impactful performance issues in Laravel applications. It occurs when you load a collection of models and then loop over them, triggering one additional query per record to load related data.

The problem:

text
SELECT * FROM orders                        -- 1 query
SELECT * FROM users WHERE id = 1            -- N queries (one per order)
SELECT * FROM users WHERE id = 2
...
SELECT * FROM users WHERE id = N

For 100 orders, that's 101 queries. For 10,000 orders, it's 10,001 queries.

Eager loading — the fix. with('relation') loads all related records in a single additional query using WHERE id IN (1, 2, 3, ...):

text
SELECT * FROM orders                        -- 1 query
SELECT * FROM users WHERE id IN (1, 2, ...)  -- 1 query

Detection tools:

  • Laravel Debugbar: Shows query count and SQL per page in development.
  • Telescope: Logs all queries with context in development.
  • Clockwork: Browser extension showing queries per request.
  • DB::enableQueryLog() + DB::getQueryLog(): Programmatic query logging.

Lazy eager loading: $orders->load('user') — adds eager loading after initial retrieval. Useful in services where you can't control the initial query.

Nested eager loading: with('items.product') — eager loads items and then product on each item.

Conditional eager loading: with(['user' => fn($q) => $q->select('id', 'name')]) — load only specific columns.

Code Example

php
<?php
// N+1 problem — DON'T do this
$orders = Order::all(); // 1 query: SELECT * FROM orders
foreach ($orders as $order) {
    echo $order->user->name; // N queries: SELECT * FROM users WHERE id = ?
}
// Result: 1 + N queries for N orders

// FIX: Eager loading
$orders = Order::with('user')->get(); // 2 queries total
foreach ($orders as $order) {
    echo $order->user->name; // no additional query — already loaded
}

// Nested eager loading
$orders = Order::with(['user', 'items.product'])->get();
// Queries: orders + users + items + products = 4 queries total regardless of data size

// Conditional eager loading (load only needed columns)
$orders = Order::with([
    'user:id,name,email',  // only select these columns
    'items' => fn($q) => $q->where('quantity', '>', 0)->with('product:id,name,price'),
])->get();

// Detecting N+1 with query log
DB::enableQueryLog();
$orders = Order::all();
foreach ($orders as $o) { echo $o->user->name; }
$log = DB::getQueryLog();
echo count($log) . " queries executed\n"; // 101 if N+1

// Stricten in tests — fail on N+1
Model::preventLazyLoading(! app()->isProduction());
// Will throw LazyLoadingViolationException if lazy loading occurs in dev/test

// Laravel Debugbar — install and it auto-shows query counts
// composer require --dev barryvdh/laravel-debugbar