Web Development 5-8 minutes

Eager Loading in Laravel 13: How to Eliminate the Eloquent N+1 Problem

Diego Cortés
Diego Cortés
Full Stack Developer & SEO Specialist
Share:
Eager Loading in Laravel 13: How to Eliminate the Eloquent N+1 Problem

Laravel eager loading can cut a listing of 100 posts from 101 SQL queries to just 2 or 3. The improvement comes from loading relationships before the loop instead of letting every property access start another database query.

What the N+1 Problem Is

Consider a view that fetches posts and prints each author's name. Post::latest()->take(100)->get() runs one query for posts, but every $post->user->name access may trigger another. You end up with one initial query plus one per row: N+1. The page is functionally correct, yet latency and database load grow with the result set.

Measure Before Changing Code

Temporarily enable query logging or use Laravel Debugbar and Telescope in development. You can also inspect response time with an external tool. Do not rely only on a small local dataset: test a realistic listing with relationships and pagination.

Basic Eager Loading with with()

The standard fix is to declare the relationship before executing the query:

$posts = Post::with('user')
    ->latest()
    ->paginate(20);

Eloquent runs one query for posts and another for their related users, then matches the results in memory. The query count no longer grows with every row. “Eager” means the relationship is planned up front, not that every possible relation should be loaded.

Nested Relationships

For a blog where posts have comments and comments belong to users, use dot notation:

$posts = Post::with('comments.user')->latest()->get();

This turns repeated lookups into a query for posts, one for comments, and one for users. Only request the branches the view needs; loading a whole object tree can still consume substantial memory.

Constraints and Selected Columns

Filter Inside with()

Pass a closure to add filters or ordering to a relationship:

$posts = Post::with(['comments' => function ($query) {
    $query->where('approved', true)->latest();
}])->get();

Short arrow functions are fine for simple expressions. Be careful with limits on a to-many relationship: a basic limit may apply to the combined relationship query rather than once per parent. For advanced cases, inspect the generated SQL and choose an appropriate query shape.

Select Only Required Columns

Fewer columns reduce the amount of data transferred, but keep the keys Eloquent needs:

$posts = Post::with('user:id,name')
    ->select('id', 'user_id', 'title')
    ->get();

Leaving out user_id prevents Eloquent from matching each author correctly. Missing foreign keys are a common reason an optimized relationship appears empty.

Lazy Eager Loading: load() and loadMissing()

load() helps when a condition decides what an already-fetched model collection needs:

$posts = Post::latest()->get();
if ($showAuthors) {
    $posts->load('user');
}

On a collection, load() retrieves the relationship in a grouped query. On a single model it works the same way. loadMissing() follows the same pattern but skips relationships that are already present:

$post->loadMissing(['user', 'comments.user']);

Aggregates Without Loading Collections

To display the number of comments, you do not need every comment model. withCount() adds a calculated attribute:

$posts = Post::withCount('comments')->latest()->get();
foreach ($posts as $post) {
    echo $post->comments_count;
}

You can also add a constrained count with an alias. This is ideal for cards and lists that show a number rather than the related records themselves.

Catch N+1 Before Production

preventLazyLoading in Development

Enable the guard in a service provider outside production:

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

When a view touches an unprepared relationship, you get an exception instead of a hidden performance problem. Configure the behavior in tests to match your team's workflow and keep the rule on during development.

Default Loading with $with

If a relationship is needed on nearly every query, declare it on the model:

protected $with = ['user'];

Use this sparingly. Always loading a large relationship can hurt endpoints that do not display it. Explicit with() calls are often clearer at the use-case level.

Complete Example: From 101 Queries to 3

For a page showing an author, comments, and a count, use Post::with('user', 'comments.user')->withCount('comments')->paginate(20). Measure the naive version, apply eager loading, and verify that the total remains stable when the page grows. Then review foreign-key indexes and paginate: eager loading reduces query overhead, but it does not replace sound database design.

Conclusion

Laravel eager loading is not about loading everything; it is about planning the relationships each response actually uses. Detect N+1 first, apply with(), use loadMissing() for conditional flows, and choose withCount() for aggregates. Eloquent can then remain expressive without turning every loop into a storm of SQL.

Categories