Object pooling and reuse patterns
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:
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 = NFor 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, ...):
SELECT * FROM orders -- 1 query
SELECT * FROM users WHERE id IN (1, 2, ...) -- 1 queryDetection 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
// 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