Optimizing Queries with Eloquent in Laravel

Video thumbnail

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.

After working with transactional operations and complex relationships between models, the next logical step was to learn how to query better, not more. And Eloquent, well used, gives us many tools to do it.

We are going to talk about the importance of optimizing database queries in Laravel and how to do it efficiently, now that we know how to do transactional operations, it is important to know now how we can perform efficient queries.

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.

The N+1 problem explained simply

The N+1 problem occurs when:

  • You make a main query.
  • For each record, Laravel executes one or more additional queries for relationships.

The result: 1 initial query + N extra queries… or even worse, 2N or 3N if you chain relationships.

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 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.

Correct use of with() to avoid N+1

When working with relationships, with() is your best ally:

$books = Book::with('post', 'post.category')
    ->select('id', 'title', 'subtitle', 'date', 'posted', 'post_id')
    ->orderBy($this->sortColumn, $this->sortDirection);

Here Laravel does:

  • One query for books
  • One for posts
  • One for categories

Without with(), each access to $b->post->category triggers additional queries, generating the famous N+1 (or worse, 2N+1).

At first glance it works, but by enabling Debugbar or logging the queries, we will see that each record causes additional queries when accessing post and category. This is the N+1 problem, which can affect performance in production.

{{ $b->id }}
{{ $b->title }}
{{ $b->subtitle }}
{{ $b->date->format('d-m-Y') }}
{{ $b->post->category->title }}
{{ $b->posted }}

Therefore, once the post is brought, the category would also be brought, which generates a 2 by N+1 problem, since for each record two additional queries are made to the database. This can be a serious problem.

In development, it is often difficult to notice because the local environment is fast, but when going to production, when multiple users are connected to the application, performance problems do arise.

Therefore, the first thing we must do is optimize queries based on the relationships we are handling, especially in listings, where this problem is more common. Optimization can be done through join, left join or, preferably, using with, which is the recommended way in Laravel.

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.

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 about optimization in Laravel

  • 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.

Now that we have seen how to perform so many database operations, let's learn about a tool with which we can debug in Laravel using a bar.

We will discuss the importance of optimizing queries in Laravel with real-world examples, seeing what works best for us: using with or join.

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español