Optimize Eloquent queries in Laravel: Eager Loading vs Lazy Loading

Video thumbnail

Content Index

Eager loading and lazy loading are two techniques available to retrieve related data when working with Eloquent models. And you need to know them in detail to use the techniques that best suit your needs; neither technique is better than the other, both are used to optimize application performance by reducing the number of database queries needed to get related data. Let's get to know them in detail.

This is something that can easily happen when using many-to-many and polymorphic relationships in Laravel.

When you work with Eloquent in Laravel, one of the keys to optimizing performance is understanding how relationships between models are loaded.

Two techniques dominate this area: lazy loading and eager loading. Neither is better than the other: everything depends on the context and how you want to balance efficiency and flexibility.

In this article, I explain their differences, how to apply them with real examples, and how to avoid the dreaded N+1 problem that can slow down your application without you realizing it.

Optimizing database queries in Laravel is not a “nice to have”, it is a real necessity when an application starts to grow. In small projects everything seems to work well, but when more users, large listings or APIs consumed from mobile devices come in, performance problems appear quickly.

⚙️ What is lazy loading in Laravel?

Also known as "on-demand loading" or "lazy loading"; this is the default behavior in Eloquent used when employing foreign key relationships.

The operation of this technique consists of Eloquent only retrieving data from the database at the moment you request it when obtaining a collection of related data (understood as a list of records coming from a relationship, for example, the list of posts given the category). That is, for each access to a related record, a separate query is executed to the database. This usually leads to the famous N+1 problem, where N+1 queries are executed to the database in the same task.

Why it is important to optimize queries in Laravel

Laravel makes working with databases much easier, but that ease can work against us if we don't pay attention to what is actually being executed underneath.

In local development, many times we don't notice problems because everything is fast. However, when you go to production and you have multiple users querying listings at the same time, every unnecessary query adds up.

The real impact in production

A poorly optimized listing can execute tens or hundreds of SQL queries without you noticing it at first glance. This translates into:

  • Increased database load.
  • Slower responses.
  • Worse user experience.
  • Scalability problems.

Relationships between models and efficient queries

We start from a relationship between Post and Category. That is, Posts inherit the category that corresponds to them. This is important to understand what data we need to load and what we should optimize in our queries, especially in listings (for example, in the index method).

If you are not using the category in your listing table, there is no need to retrieve it from the database. For example:

<td>{{ $p->id }}</td>
<td>{{ $p->title }}</td>
<td>{{ $p->posted }}</td>

If you don't use $p->category->title, you don't need to load the relationship, avoiding unnecessary queries.

This is something I learned quickly when working with large listings: every unnecessary relationship is an extra query waiting to happen.

Real example: Book, Post and Category

In a real case, I have a relationship where:

  • Book belongs to Post
  • Post belongs to Category

The category is not directly assigned to the Book, but comes through the Post. This avoids redundancy and maintains data integrity.

class Book extends Model
{
    public function post()
    {
        return $this->belongsTo(Post::class)
            ->select(['id', 'url_clean', 'title', 'category_id']);
    }
}
class Post extends Model
{
    public function category()
    {
        return $this->belongsTo(Category::class)
            ->select(['id', 'url_clean', 'title']);
    }
}

In this case, the Book's category is not directly assigned because it is already brought through the Post. This avoids redundancy in the database and maintains information integrity.

Eloquent Quick Reference

For your subscribers, this little "cheat sheet" is very valuable to consult while they write code:

1. Prevent Lazy Loading (Global Level)

In your AppServiceProvider, add this to the boot() method to detect errors during development:

Model::preventLazyLoading(!app()->isProduction());

2. Basic Eager Loading

// Load a relationship
$posts = Post::with('category')->get();
// Load multiple relationships
$posts = Post::with(['category', 'tags'])->get();

3. Eager Loading with Conditions (Nested)

When you need to filter what you fetch in the relationship:

$tutorials = Tutorial::with(['sections.classes' => function ($query) {
   $query->where('posted', 'yes')->orderBy('orden');
}])->get();

Key Differences: has, with, and whereHas

Many people get confused by these three methods. Here is the straightforward explanation:

  • with(): Loads data. It does not filter the main results, it only brings the related ones to avoid the N+1 problem.
  • has(): Filters. It only returns the main models that have records in the relationship (e.g., User::has('posts') only brings users with at least one post).
  • whereHas(): Filters with conditions. Same as has(), but you can add specific filters (e.g., User::whereHas('posts', fn($q) => $q->where('status', 'published'))).

How lazy loading works

Imagine you are displaying a list of posts and, for each one, you need the name of its category:

$posts = Post::paginate(10);
@foreach ($posts as $p)
  {{ $p->category->title }}
@endforeach

Although it seems innocent, this code executes one query for each access to the relationship, generating the classic N+1 problem:
one main query + one for each related record.

The N+1 problem explained with real examples

In one of my projects, a posts dashboard, this behavior caused more than 15 queries for a single paginated page.
Performance plummeted.

To detect the origin, I enabled the following function in the AppServiceProvider:

Model::preventLazyLoading(app()->isProduction());

Thanks to this, Laravel throws an exception when it tries to lazy load relationships in production.

The typical message is:

Attempted to lazy load [category] on model [App\Models\Post] but lazy loading is disabled.

How to detect N+1 queries with DB::listen or Debugbar

If you want to audit your queries, you can use:

DB::listen(function ($query) {
  echo $query->sql;
});

Or, if you prefer a visual interface, install Laravel Debugbar, ideal for local environments.

This way you'll know how many queries each view executes and you can act before load times skyrocket.

⚡ What is eager loading and when to use it

Video thumbnail

Eager loading or anxious loading allows retrieving all necessary relationships in a single query.

In this way, Laravel prepares the data from the beginning, avoiding the N+1 problem.

How to apply eager loading with the with() method

The most common way is using the with() method:

$posts = Post::with('category')->paginate(10);

With this line, Eloquent loads the posts along with their categories in a single operation.

When accessing Eloquent relationships as properties, the relationship data is "deferred loaded" or what is known as lazy loading.

This means that the relationship data is not actually loaded until the property is first accessed.

However, Eloquent can "eager load" relationships at the moment it queries the primary model. Eager loading alleviates the N+1 queries problem.

To illustrate the N+1 queries problem, consider the following models:

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\URL;
class Tutorial extends Model
{
    use HasFactory;
    protected $fillable = [***];
    public function sections()
    {
        return $this->hasMany(TutorialSection::class)->orderBy('orden');
    }
}
class TutorialSection extends Model
{
    use HasFactory;
    protected $fillable = [***];
    public function tutorial()
    {
        return $this->belongsTo(Tutorial::class);
    }
    public function classes()
    {
        return $this->hasMany(TutorialSectionClass::class)->orderBy('orden');
    }
}
class TutorialSectionClass extends Model
{
    use HasFactory;
    protected $fillable = [***];
    public function tutorialSection()
    {
        return $this->belongsTo(TutorialSection::class);
    }
    public function comments()
    {
        return $this->hasMany(TutorialSectionClassComment::class);
    }
}

The query for:

foreach ($tutorials as $key => $t)
  foreach ($t->sections as $key => $s)

Will execute one query to retrieve all tutorials:

$tutorials = Tutorial->get();

Then, we iterate over all tutorials and, in turn, get the sections for each tutorial; if we have 25 tutorials, it would be one additional database query for each tutorial to get the sections; there would be 26 queries, 1 for the tutorial with the sections and 25 more for each section; here we have the problem known as N+1 since, depending on the number of tutorials we have, we have to make the same number of queries to the database to get their sections; but, things can get more complicated; if we now want to get the classes:

then, if we want to get the classes:

foreach ($s->classes->get() as $k => $c)

The result would be catastrophic; luckily, in Laravel we can solve this problem so that it only returns a single query to the database.

Eager Loading

In Laravel, Eager Loading is an optimization technique to reduce the number of database queries. By default, when retrieving data with relationships in Laravel, Lazy Loading is used, which can result in the N+1 query problem as explained previously.

Eager Loading is a data loading technique in which related data is loaded in advance to avoid the need to load it later when accessed in a single query, therefore, this technique solves the previous N+1 problem; for this, we must use the with function indicating the name of the relationship in the model:

$tutorial = Tutorial::with('sections');

Eager Loading Multiple Relationships

If we want to include additional relationships, we can specify them in an array; if the relationships are nested, we can do the following:

$tutorial = Tutorial::with(['sections','sections.classes' ])

Just as you can see, we place the first relationship:

sections

And then the innermost relationship:

sections.classes

Eager Loading Conditions

Many times it is necessary to indicate additional conditions, for this, we can specify a callback function in which we place the internal conditions, in the following example, we want the classes to be posted or published:

$tutorial = Tutorial::with('sections')->with(['sections.classes' => function ($query) {
           $query->where('posted', 'yes');
       }])->find($tutorial->id);

And with this, you can easily create your relationships when loading the parent relationship; this is particularly useful when you need to build a Rest API and you initially need all the data, including the parent and child relationship.

Define eager loading directly in the models

In the models, we can predefine the use of eager loading as we saw before; for this, we use the with property:

class Tutorial extends Model
{
    ***
    protected $with = ['sections'];
}

The with method for loading Relationships with Eager Loading

When working with relationships in Eloquent, we often need to load all the relationships of a specific model or of several; we are talking about having a main model and one or more others related by a foreign key and we want to load all this data in a single query, of course, using Eloquent and avoiding the use of Joins for ease of the query; imagine you have an online store with products, categories, and tags. You want to get all products along with their related categories and tags. This is where Laravel's eager loading comes into play, since, through the with method, we can specify the relationships from the main query, obtaining the data completely organized and without repeated records as would happen if we used JOINs.

Eager loading allows loading all necessary relationships when performing the initial query. Instead of executing one query for each model in the collection, Eloquent performs a single additional query to load all relationships. This significantly improves the efficiency and speed of your application.

Suppose we have the following models: Product, Category, and Tag. We want to get all products along with their categories and tags:

$products = Product::with(['category', 'tags'])->get();

And now, we can access ALL its relationships without the need to perform additional queries:

foreach ($products as $product) {
    echo "Product: {$product->name}\n";
    echo "Category: {$product->category->name}\n";
    foreach ($product->tags as $tag) {
        echo "Tag: {$tag->name}\n";
    }
    echo "\n";
}

In this way, we can optimize queries by avoiding problems like the N+1 in Laravel and thus have efficient queries; in this way, we can easily perform additional operations such as saving the data made in a single query to cache by using optimized SQL queries.

Practical example: categories and posts in Laravel

Instead of doing this:

$categories = Category::paginate(10);
@foreach ($categories as $c)
  {{ $c->posts }}
@endforeach

We can optimize it like this:

$categories = Category::with('posts')->paginate(10);

However, be careful: if your posts contain large fields like content, you could overload the query.

To do this, select only the necessary columns:

$posts = Post::with('category:id,title')->paginate(10);

Nested eager loading and with conditions

Eloquent allows loading nested or filtered relationships easily:

Tutorial::with('sections')
   ->with(['sections.classes' => function ($query) {
       $query->where('posted', 'yes')->orderBy('orden');
   }])
   ->find($tutorial->id);

This way, you avoid redundant queries even in multi-level structures.

Eager vs Lazy loading: comparison and performance

Video thumbnail
TechniqueQueries generatedPerformanceRecommended usage
Lazy loading1 + N (one for each relationship)Slower if there are many relationshipsWhen you only access one or a few specific relationships
Eager loading1 or few queriesFaster in large collectionsWhen you need to display several relationships at the same time

The difference is abysmal when working with complex relationships.

Related methods: has(), with() and whereHas()

In Laravel, the model layer with Eloquent is one of the richest layers of the MVC that Laravel includes, and rightly so, since it is the data layer, which connects to the database to manage it; there are many methods available in Eloquent, but, let's look at 3 that can be confused: has, with, and whereHas.

1. "Has": Filtering models based on relationships

The has() method is used to filter the selected models based on a relationship. It works similarly to a normal WHERE condition but with a relationship. 

If you use has('relation'), it means you only want to get the models that have at least one related model in this relationship.

For example, let's consider a blog system with two tables: "posts" and "comments". If we want to get all users who have at least one comment, we can do it as follows:

$users = User::has('comments')->get(); 
// Only users who have at least one comment will be included in the collection

In short, it is a kind of conditional in which users who have at least one comment are obtained; in this example, comments is a relationship of the users.

2. "With": Loading relationships efficiently (eager loading)

The with() method is used to load relationships along with the main model, we previously saw the N+1 problem in Laravel which is solved using the with method.

Basically, along with the main model, Laravel will load the relationships you specify. This is especially useful when you have a collection of models and want to load a relationship for all of them.

$users = User::with('posts')->get();
foreach ($users as $user) {
  // The posts are already loaded and no additional query is executed
  $user->posts;
}

3. "WhereHas": Filtering based on relationships with additional conditions

The whereHas() method works similarly to has(), but allows you to specify additional filters for the related model. You can add custom conditions to check on the related model.

For example, if we want to get all users who have posts created after a specific date, we can do it like this:

$users = User::whereHas('posts', function ($query) {
    $query->where('created_at', '>=', '2026-01-01 00:00:00');
})->get();
// Only users who have posts from 2026 onwards will be included

What is whereHas?

Video thumbnail

In short, has (which I think is the first time I use it in a course) is the way to query relationships. It's that simple.

In a few words, we cannot query relationships using a where. Although there is a small consideration: if you were using a join, you could perfectly use where, which would be like, as they say, the traditional way all along.

But in this case we are working with with, and therefore, we cannot do it that way:

Book::with(['post', 'post.category'])
         ->when($this->category_id, function (Builder $query, $category_id) {
                $query->whereHas('post', function ($q) use ($category_id) {
                    $q->where('category_id', $category_id);
                });
            })

This is the relationship with the models:

class Book extends Model
{
    ***
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}
class Post extends TaggableModel
{
    ***
    public function category()
    {
        return $this->belongsTo(Category::class);
        //->select('id', 'title', 'slug');
    }
}

Summary: Why do we use whereHas?

In short, whereHas is the mechanism we have to make a where condition on a relationship.

Stick with that: that's why we use whereHas.
For everything else, there's Mastercard... sorry, there's Eloquent.

We already have this problem in our dashboard module, on the one hand, we have the main query:

app\Http\Controllers\Dashboard\PostController.php

public function index()
{
    if(!auth()->user()->hasPermissionTo('editor.post.index')){
        return abort(403);
    }
    $posts = Post::paginate(10);
    return view('dashboard/post/index', compact('posts'));
}

And from the view, we reference the category, by default, Laravel uses the lazy loading technique to get the related data, therefore, every time a query is made, an additional query will be made, from the listing, we are getting the category and with this an additional query for each post on the page:

resources\views\dashboard\post\index.blade.php

@foreach ($posts as $p)
     ****
     <td>
        {{ $p->category->title }}
***

Which is the N+1 problem, where N in our example is the page size, about 10 representing the categories obtained from the post and the 1 is the main query to get the paginated data.

Luckily, in modern versions Laravel allows detecting this problem very easily through the following configuration:

app\Providers\AppServiceProvider.php

<?php
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
***
class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading(app()->isProduction());
    }
}

With the AppServiceProvider we can load essential classes of our project to integrate them into the application.

So, if we now try to access the previous page, we will see an error on the screen like the following:

Attempted to lazy load [category] on model [App\Models\Post] but lazy loading is disabled.

The N+1 problem detection system in Laravel is not perfect, since if we only had a 1-level pagination, the previous exception would not occur.

With an additional trick, we can see the queries made to resolve a client request:

routes/web.php

DB::listen(function ($query){
    echo $query->sql;
  //  Log::info($query->sql, ['bindings' => $query->bindings, 'time' => $query->time]);
});

We can also use the Debugbar extension, but we will see that in the next chapter, if you enable the previous script, you will see that more than 15 queries occur, one of them for the authenticated user's session, permissions and roles, the one for the posts, and 10 for the categories if you have a 10-level pagination. This is great for detecting the problem but with the inconvenience that our category detail page no longer works, to fix it, we are going to introduce the next topic.

Let's create another example, we are going to use the posts relationship we have in the category:

app\Models\Category.php

class Category extends Model
{
   ***
    function posts() {
        return $this->hasMany(Post::class);
    }
}

If we get the relationship from the view:

resources\views\dashboard\category\index.blade.php

@foreach ($categories as $c)
    ***
        <td>
            {{ $c->posts }}

We will see the previous exception, so, we get the posts along with the categories:

app\Http\Controllers\Dashboard\CategoryController.php

$categories = Category::with('posts')->paginate(10);

The problem with this scheme is that it will bring all the posts associated with a category, and a post is a somewhat heavy relationship, since it contains the content column with all the HTML content, and if we add the 10 categories in the listing, the problem multiplies.

There are several ways in which we can specify the columns we want to obtain from the secondary relationship:

$posts = Post::with('category:id,title')->paginate(10);
$posts = Post::with(['category' => function($query){
   // $query->where('id',1);
   $query->select('id','title');
}])->paginate(10);

Although these schemes are not supported by the posts relationship of the categories:

$categories = Category::with('posts:id,title')->paginate(10);
$categories = Category::with(['posts' => function($query){
    // $query->where('id',1);
    $query->select('id','title');
}])->paginate(10);

For now, we cannot solve the problem and with it the exception since for that we need to either change the request to use JOINs, or present the next topic which is Eager Loading which we will see next.

Eager Loading

With this process we can perform all operations in a single query, if we go back to the previous example, which has N+1 queries to the database, we will only perform a single query and with this, improve the performance of the application, for this, we must specify the relationship when performing the main query:

app\Http\Controllers\Dashboard\PostController.php

$posts = Post::with(['category'])->paginate(10);

If we go to our category detail page, we will see that it works correctly.

This function has many implementations, for example, if we have a nested relationship:

class Tutorial extends Model
{
    ***
}

We can also define the use of this technique by default in the model:

class Post extends Model
{
    protected $with = ['category'];
}

The with() method can be extended in more complex relationships, like the following one that has a two-level relationship:

class Tutorial extends Model
{
  ***
    public function sections()
    {
        return $this->hasMany(Tutorial::class);
    }
}
class TutorialSection extends Model
{
  ***
    public function tutorial()
    {
        return $this->belongsTo(Tutorial::class);
    }
    public function classes()
    {
        return $this->hasMany(Tutorial::class);
    }
}
class TutorialSectionClass extends Model
{
    ***
    public function tutorialSection()
    {
        return $this->belongsTo(TutorialSection::class);
    }
}

We can make queries in the following way, indicating more than one relationship to obtain:

$posts = Post::with(['categories','tags'])->get();

Or if you want to place a condition on some of the relationships, you can implement a callback in the following way:

Tutorial::with('sections')->with(['sections.classes' => function ($query) {
     $query->where('posted', 'yes');
     $query->orderBy('orden');
    }])->where('posted', 'yes')->find($tutorial->id);
}

Prevent Lazy Loading in Laravel 3 ways

In this post, we will see how we can detect Lazy Loading in Laravel; to do this, we will start from the following code:

app\Http\Controllers\Dashboard\PostController.php

public function index()
{
    $posts = Post::paginate(10);
    return view('dashboard/post/index', compact('posts'));
}

Where, a post has a foreign relationship with the categories:

class Post extends Model
{
    use HasFactory;
    protected $fillable = ['title', 'slug', 'content', 'category_id', 'description', 'posted', 'image'];
    public function category()
    {
        return $this->belongsTo(Category::class);
    }
}

From the view, we reference the category, by default, Laravel uses the lazy loading technique to obtain the related data therefore, every time a query is made, an additional query will be made, from the list, we are obtaining the category and with this an additional query for each post on the page:

resources\views\dashboard\post\index.blade.php

@foreach ($posts as $p)
     ****
     <td>
        {{ $p->category->title }}
***

Which is the N+1 problem, where N in our example is the size of the page, about 10 representing the categories obtained from the post and 1 is the main query to obtain the paginated data.

1. Using the AppServiceProvider

Luckily, Laravel in modern versions allows you to detect this problem very easily through the following configuration:

app\Providers\AppServiceProvider.php

<?php
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
***
class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading(app()->isProduction());
    }
}

With the AppServiceProvider we can load essential classes from our project to integrate them into the application.

2. Viewing the SQL queries

So, if we now try to access the previous page, we will see an error on the screen like the following:

Attempted to lazy load [category] on model [App\Models\Post] but lazy loading is disabled.

The N+1 problem detection system in Laravel is not perfect, since if we only had 1 level pagination, the previous exception would not occur.

Another way to detect the N+1 problem is with an additional trick, we can see the queries made to resolve a client request:

routes/web.php

DB::listen(function ($query){
    echo $query->sql;
  //  Log::info($query->sql, ['bindings' => $query->bindings, 'time' => $query->time]);
});

And from the browser, we would see a query every time we reference the category from the post.

3. Viewing SQL queries

We can also use the debugbar extension, if you enable the previous script, you will see that more than 15 queries occur, one of them for the session of the authenticated user, the one for the posts and 10 for the categories if you have a 10-level pagination. This It's great to detect the problem but with the drawback that our detail page for the category.

⚠️ Common errors and good practices in Eloquent

Avoid unnecessary queries

Use with() only when you really need to load relationships.

Don't abuse eager loading with heavy or little-used relationships.

Load only the necessary columns

You can limit the columns with:

Post::with('category:id,title');

This reduces the weight of each query.

Activate preventLazyLoading only in production

Model::preventLazyLoading() helps detect errors in development,
but only activate it in production if you are sure that your relationships are well optimized.

Conclusion

It is important to mention that there is no technique better than the other, since everything depends on what you want to do, but we can simplify it as follows: if you have a collection of related records using an FK as in the previous example and you are not going to use the foreign relationship, the technique you should use would be lazy loading, but, if you are going to use the collection with the related records, you must use eager loading.

Finally, for each relationship specified in with() it only adds one additional query, also remember to specify the columns as much as possible when obtaining the relationships.

There is no better technique than the other.

  • Use lazy loading when you don't need all the relationships.
  • Use eager loading when your view or API depends on multiple related data.

Laravel gives you the flexibility to combine both and obtain the best performance depending on the context.

Eloquent vs Query Builder

Laravel offers us two clear paths to optimize queries: Eloquent and Query Builder. Neither is better by default, it all depends on the context.

  • Eloquent (with): brings the defined relationships avoiding the N+1 queries problem.
  • Query Builder (join, leftJoin): also allows optimization through joins, although the syntax is different.

Using with or joins: When to use joins or leftJoin

In some cases, especially in complex listings or APIs, I prefer to use leftJoin directly:

Book::select(
   'books.title',
   'books.subtitle',
   'books.date',
   'books.posted',
   'file_payments.payments'
)
->leftJoin('file_payments', function ($join) use ($user) {
   $join->on('books.id', 'file_payments.file_paymentable_id')
        ->where('file_paymentable_type', Book::class)
        ->where('file_payments.user_id', $user->id);
})
->where('posted', 'yes')
->get();

Here I control exactly what data goes into the query and avoid loading complete models that I don't need.

Select only the necessary columns

One of the most common mistakes is to use SELECT * in listings.

Avoid SELECT * in listings

Fields like content, body or long texts should not be loaded if they are not used:

$books = Book::select(
   'title',
   'subtitle',
   'date',
   'url_clean',
   'posted',
   'price'
)->get();

This reduces:

  • Response size.
  • Memory usage.
  • Execution time.

Query optimization in REST APIs

In administrative dashboards, where many records are loaded, this point makes a huge difference. In my experience, just limiting columns already shows a clear improvement.

All of the above applies even more when we talk about APIs.

What data to send and what not to send

An API should:

  • Send only what the client needs.
  • Avoid heavy fields.
  • Reduce the number of queries.

If an endpoint is for listings, there is no point in returning the full content of the resource.

Performance on mobile devices

On mobile, every byte counts:

  • Less data → less loading time.
  • Fewer queries → better battery and experience.
  • Faster responses → smoother apps.

Tools to detect slow queries

Before optimizing, you have to see what is really happening.

  • Laravel Telescope
  • Telescope allows you to see:
    • Executed queries.
    • Duplicates.
    • Execution time.
  • It is ideal for quickly detecting N+1 in development.
  • Debugbar and Clockwork
    • Laravel Debugbar and Clockwork are excellent alternatives:
      • Debugbar shows queries directly in the view.
      • Clockwork works as a browser extension.

On more than one occasion, thanks to these tools, I discovered unnecessary queries that were not evident at first glance.

Final best practices for optimizing Eloquent

  • Quick optimization checklist
    • Use with() for relationships.
    • Avoid relationships you don't use.
    • Select only the necessary columns.
    • Use joins when you need maximum control.
    • Optimize listings and APIs.
    • Always review queries with debug tools.

Optimization recommendations

1. Use with or joins

Whenever you work with relationships and listings, retrieve only the necessary data from the database. For example:

Post::with('category:id,url_clean,title');

Or in the model, limiting the relationship fields:

public function category()
{
   return $this->belongsTo(Category::class)
       ->select(['id', 'url_clean', 'title']);
}

2. Select only the necessary columns

Do not retrieve large fields like content if you are not going to use them in the listing:

$books = Book::select(
   'books.title', 'books.subtitle', 'books.date', 
   'books.url_clean', 'books.description', 'books.image', 
   'books.path', 'books.page', 'books.posted', 
   'books.price', 'books.price_offers', 'books.post_id'
)->get();

This reduces the amount of data transferred and improves performance.

3. Avoid errors with select and relationships

These recommendations also apply to REST APIs, where it is critical:

  • Retrieve only the data the client needs.
  • Avoid loading unnecessary heavy content.
  • Reduce the number of queries to improve speed and resource consumption.

Example with leftJoin:

Book::select(
   'books.title', 'books.subtitle', 'books.date', 'books.url_clean', 
   'books.description', 'books.image', 'books.path', 'books.page', 
   'books.posted', 'books.price', 'books.price_offers', 'books.post_id',
   DB::raw('DATE_FORMAT(file_payments.created_at, "%d-%m-%Y %H:%i") as date_buyed'),
   'file_payments.payments'
)
->leftJoin('file_payments', function ($leftJoin) use ($user) {
   $leftJoin
       ->on('books.id', 'file_payments.file_paymentable_id')
       ->where("file_paymentable_type", Book::class)
       ->where('file_payments.user_id', $user->id)
       ->where('file_payments.unenroll');
})
->where('posted', 'yes')
->get();

Rest API and Query Optimization

If you cannot pass the limited fields directly from the query, then you can do it from the relationship, as I showed you before. Everything we mentioned about query optimization also applies to a Rest API, and it makes a lot of sense, especially when dealing with data listings.

For example, a query to the books could be:

$books = Book::select(
    'books.title',
    'books.subtitle',
    'books.date',
    'books.url_clean',
    'books.description',
    'books.image',
    'books.path',
    'books.page',
    'books.posted',
    'books.price',
    'books.price_offers',
    'books.post_id',
    DB::raw('DATE_FORMAT(file_payments.created_at, "%d-%m-%Y %H:%i") as date_buyed'),
    'file_payments.payments'
)->leftJoin('file_payments', function ($leftJoin) use ($user) {
    $leftJoin
        ->on('books.id', 'file_payments.file_paymentable_id')
        ->where("file_paymentable_type", Book::class)
        ->where('file_payments.user_id', $user->id)
        ->where('file_payments.unenroll');
})->where('posted', 'yes')
->get();

In this case, I am using leftJoin to bring additional data, and then I specify with select only the fields I really need. I did not use with because I am handling the joins directly, and the idea is to bring only the necessary data for the listing, without including heavy fields like content, which will not be used here.

Final best practices for optimizing Eloquent

  • Quick optimization checklist
    • Use with() for relationships.
    • Avoid relationships you don't use.
    • Select only the necessary columns.
    • Use joins when you need maximum control.
    • Optimize listings and APIs.
    • Always review queries with debug tools.

Optimization recommendations

1. Use with or joins

Whenever you work with relationships and listings, retrieve only the necessary data from the database. For example:

Post::with('category:id,url_clean,title');

Or in the model, limiting the relationship fields:

public function category()
{
   return $this->belongsTo(Category::class)
       ->select(['id', 'url_clean', 'title']);
}

2. Select only the necessary columns

Do not retrieve large fields like content if you are not going to use them in the listing:

$books = Book::select(
   'books.title', 'books.subtitle', 'books.date', 
   'books.url_clean', 'books.description', 'books.image', 
   'books.path', 'books.page', 'books.posted', 
   'books.price', 'books.price_offers', 'books.post_id'
)->get();

This reduces the amount of data transferred and improves performance.

3. Avoid errors with select and relationships

These recommendations also apply to REST APIs, where it is critical:

  • Retrieve only the data the client needs.
  • Avoid loading unnecessary heavy content.
  • Reduce the number of queries to improve speed and resource consumption.

Example with leftJoin:

Book::select(
   'books.title', 'books.subtitle', 'books.date', 'books.url_clean', 
   'books.description', 'books.image', 'books.path', 'books.page', 
   'books.posted', 'books.price', 'books.price_offers', 'books.post_id',
   DB::raw('DATE_FORMAT(file_payments.created_at, "%d-%m-%Y %H:%i") as date_buyed'),
   'file_payments.payments'
)
->leftJoin('file_payments', function ($leftJoin) use ($user) {
   $leftJoin
       ->on('books.id', 'file_payments.file_paymentable_id')
       ->where("file_paymentable_type", Book::class)
       ->where('file_payments.user_id', $user->id)
       ->where('file_payments.unenroll');
})
->where('posted', 'yes')
->get();

Rest API and Query Optimization

If you cannot pass the limited fields directly from the query, then you can do it from the relationship, as I showed you before. Everything we mentioned about query optimization also applies to a Rest API, and it makes a lot of sense, especially when dealing with data listings.

For example, a query to the books could be:

$books = Book::select(
    'books.title',
    'books.subtitle',
    'books.date',
    'books.url_clean',
    'books.description',
    'books.image',
    'books.path',
    'books.page',
    'books.posted',
    'books.price',
    'books.price_offers',
    'books.post_id',
    DB::raw('DATE_FORMAT(file_payments.created_at, "%d-%m-%Y %H:%i") as date_buyed'),
    'file_payments.payments'
)->leftJoin('file_payments', function ($leftJoin) use ($user) {
    $leftJoin
        ->on('books.id', 'file_payments.file_paymentable_id')
        ->where("file_paymentable_type", Book::class)
        ->where('file_payments.user_id', $user->id)
        ->where('file_payments.unenroll');
})->where('posted', 'yes')
->get();

In this case, I am using leftJoin to bring additional data, and then I specify with select only the fields I really need. I did not use with because I am handling the joins directly, and the idea is to bring only the necessary data for the listing, without including heavy fields like content, which will not be used here.

Differences and Considerations

  • When we make a listing in the dashboard or for a Rest API, we do not need to bring full content fields that would only be used in the book detail. This reduces the load on the database and optimizes the API response.
  • If we need to show full content, only then do we include the content field, for example, in the book detail.
  • On mobile devices, loading less data is crucial because it reduces the page weight and improves the user experience.

Differences and Considerations

  • When we make a listing in the dashboard or for a Rest API, we do not need to bring full content fields that would only be used in the book detail. This reduces the load on the database and optimizes the API response.
  • If we need to show full content, only then do we include the content field, for example, in the book detail.
  • On mobile devices, loading less data is crucial because it reduces the page weight and improves the user experience.

❓ Frequently Asked Questions

  • Which is faster: eager or lazy loading?
    • Eager loading, because it groups the queries into a single SQL operation.
  • Can I combine both techniques?
    • Yes, Eloquent allows you to use eager loading for some relationships and lazy loading for others, depending on your needs.
  • How to solve the "lazy loading is disabled" error?
    • Temporarily activate the lazy loading mode in development or adjust your queries with with() to avoid the error.
  • Is it better to use Eloquent or Query Builder?
    • It depends on the case. For relationships, Eloquent with with() is ideal. For complex or highly optimized queries, Query Builder may be better.
  • Should I always use with()?
    • Only when you are actually going to use the relationship. Loading unnecessary relationships is also a mistake.
  • Where is the N+1 problem most noticeable?
    • In large listings and in production, especially with many concurrent users.
  • Does this also apply to APIs?
    • Yes, even more so. APIs must be lightweight and efficient.

Conclusion

Optimizing queries with Eloquent in Laravel is not complicated, but it does require discipline. Especially in listings and APIs, small decisions make a big difference.

In my experience, a good understanding of relationships, avoiding redundancies, and retrieving only the necessary data improves:

  • Performance.
  • Scalability.
  • Project organization.
  • Always optimize your queries, especially in listings and APIs.
  • Use with or joins to reduce the N+1 problem.
  • Retrieve only the columns you are going to use.
  • Avoid redundancy in your models and relationships.
  • This improves performance, modularity, and the organization of your project.

And once this is clear, the next natural step is to learn how to debug: tools like Debugbar or Telescope become indispensable.

The next step is the use of unit tests in Laravel.

Experiencing performance issues in Laravel? Learn to master Eager Loading vs Lazy Loading. Fix the N+1 error in your Eloquent queries and optimize your app.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español