The N+1 Query Problem
The single most common Laravel performance issue. It’s easy to write and easy to miss without checking query counts:
// N+1: fires one query per post to get its author
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name;
}
// Fixed: eager load the relationship
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name;
}
Selecting Only What You Need
// Pulls every column, even ones you don't use
$users = User::all();
// Only pulls what's actually needed
$users = User::select('id', 'name', 'email')->get();
Using chunk() for Large Datasets
// Loads everything into memory at once — dangerous at scale
$users = User::all();
// Processes in manageable batches
User::chunk(500, function ($users) {
foreach ($users as $user) {
$user->sendReminderEmail();
}
});
Avoiding Unnecessary Model Hydration
// Hydrates full Eloquent models just to check existence
if (User::where('email', $email)->first()) { ... }
// Cheaper: exists() runs a lighter query
if (User::where('email', $email)->exists()) { ... }
Counting Related Records Efficiently
// N+1 again: one COUNT query per post
$posts = Post::all();
foreach ($posts as $post) {
echo $post->comments->count();
}
// withCount does it in a single query
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
Debugging Query Counts
DB::enableQueryLog();
// ... run your code
dd(DB::getQueryLog());
Or use Laravel Debugbar / Telescope in local development to catch N+1 patterns visually before they reach production.
Caching Expensive Queries
$topProducts = Cache::remember('top-products', 3600, function () {
return Product::orderByDesc('sales_count')->limit(10)->get();
});
Conclusion
Most Eloquent performance problems come from a small set of repeatable mistakes: missing eager loading, over-fetching columns, and re-querying data that could be cached. Check your query counts during development, not after a production slowdown forces the issue.