Fix the Laravel Eloquent N+1 problem by mastering eager loading. Learn how to optimize database queries, apply constraints, and speed up your applications.
Last month, a dashboard in one of our client projects started hanging for nearly four seconds on every page load. After checking the logs, I realized the application was firing over 150 individual database queries just to render a simple list of users and their recent activity. It was a classic case of the N+1 query problem, a silent performance killer that often sneaks into production when you least expect it.
The N+1 issue happens when you fetch a collection of models and then access a relationship for each one inside a loop. If you have 50 users and you call $user->profile inside a foreach loop, Laravel executes one query to get the users and then 50 additional queries to fetch each profile.
It’s easy to overlook during development when your local database only has three rows. However, as your data scales, those extra queries add up to significant latency. If you haven't tackled this yet, Eloquent Performance Optimization: Solving the N+1 Problem is a great place to start identifying these bottlenecks before they hit your users.
The most straightforward fix for Laravel Eloquent performance is eager loading. By using the with() method, you tell Laravel to fetch all related records in a single, secondary query rather than executing a new query for every iteration.
Instead of this:
PHP$users = User::all(); #6A9955">// 1 query foreach ($users as $user) { echo $user->profile->bio; #6A9955">// N queries }
You should do this:
PHP$users = User::with('profile')->get(); #6A9955">// 2 queries total foreach ($users as $user) { echo $user->profile->bio; }
This simple change drops your query count from N+1 down to exactly two. It’s almost always the first step in effective database optimization.
Sometimes, you don't need all related data. Maybe you only want to eager load profiles that have a specific status. You can pass an array to with() to apply constraints directly to the eager loading process.
PHP$users = User::with(['profile' => function ($query) { $query->where('is_active', true); }])->get();
This technique is essential when you're dealing with large datasets. By filtering the related records early, you reduce the memory footprint of your application and lower the load on your database. If you're struggling with complex relationships, Querying Related Data: Mastering Eager Loading in Laravel goes deeper into these advanced patterns.
There are times when you don't know if you'll need the relationship until after the initial query has already executed. In these cases, you can use load() to perform "lazy eager loading."
PHP$users = User::all(); if ($someCondition) { $users->load('profile'); }
This is incredibly useful in conditional logic where you want to avoid unnecessary joins or data fetching. Just be careful—if you use this inside a loop, you’re right back to the N+1 problem. Always ensure you are loading the relationship on the entire collection at once, not on individual items.
| Method | Execution Timing | Use Case |
|---|---|---|
with() | Initial query | Known relationships you'll always need. |
load() | After initial query | Conditional loading based on runtime state. |
loadCount() | After initial query | When you only need the count, not the data. |
withCount() | Initial query | Getting record counts without fetching models. |
I’ve learned the hard way that it's easy to forget with() when you're moving fast. To prevent this, I always enable query logging in my local environment. Even better, use Model::preventLazyLoading(!app()->isProduction()); in your AppServiceProvider. This throws an exception if you accidentally trigger an N+1 query, forcing you to fix it before you even commit your code.
While eager loading is a massive win for Laravel performance, remember that it isn't a silver bullet. If your database schema isn't properly optimized, even the most efficient eager loading can't save you. You should complement your query changes with Database Indexing Strategies: Optimizing Laravel Query Performance to ensure the underlying lookups are as fast as possible.
I'm still experimenting with tools like Laravel Eloquent Query Optimization via AST-based Static Analysis to catch these issues at the CI level. It’s a bit more setup, but it’s worth it if you’re working on a team where manual code review might miss a stray relationship call.
Q: Does eager loading always make queries faster? A: Usually, yes. However, if you eager load a massive relationship that you don't actually use, you might increase memory usage and transmission time. Only load what you need.
Q: Can I eager load nested relationships?
A: Absolutely. Use dot notation, like User::with('profile.address')->get(), to load multiple levels of relationships in one go.
Q: What is the difference between with() and load()?
A: with() is used when building the query, while load() is used on an existing collection or model instance after the initial query has already run.
I still find myself occasionally missing a with() call on a refactor, but the preventLazyLoading check has saved me from shipping bad code more times than I can count. Keep an eye on your debug bar, and don't assume your code is efficient just because it "looks" clean.
Laravel DTO hydration using the Reflection API ensures runtime type safety. Stop passing arrays and start building deterministic, validated data objects today.