Laravel, Programming

The Importance of Eager Loading Laravel Relationships

Problem

Page took over 30 seconds to load locally on a local Vagrant virtual machine. After investigating the results were astonishing, over 1,000 queries executed in order to load the page. Not only was there an extraordinary number of queries executing, but also no constraints on the number of records loaded from each relationship.

Test and production environments didn’t show noticeable signs of slowness, but was clear on my local virtual machine that something was wrong.

Research

My research began using the Laravel debugbar. The debugbar helped me discover the amount of queries running for each page. Laravel debugbar has a query collector that conveniently displays the query count. An additional bonus is the number of duplicate queries along with which class called the query. Using these pieces of information I began tracking down the cause.

Cause

The cause was the classic N+1 problem. Essentially a loop was calling a relationship that had not been eager loaded. Each iteration of the loop caused the app to query the database.

Solution

The problem was easily solved using Laravel’s eager loading along with eager loading constraints. ¬†Eager loading is loading your data up front before accessing the relationship. Because the relationship data was loaded up front accessing the relationship now pulls the value from memory instead of querying the database.

Eager loading constraints tell Laravel to only load specific records within a relationship. When you don’t need every record that a relationship has. E.g. Authors have books, but you only need love and thriller book types.

Eager loading with constraints reduced:

  • Query count from over 1k queries to 50 a whopping 2000% decrease in queries.
  • Number of records loaded from 7200 to 24.
  • Memory consumed by page from 140MB to 14MB.
  • Page load from 30 seconds to 2.5 seconds.

Example

Record labels have artists and artists have songs. If you wanted to get all artists on a record label along with their songs, but only the songs that reached top 10 on the billboards

Non-eager loaded:

$artists = Artist::take(500)->get();
$artist->songs;

Eager loaded without constraints (returns all songs):

$artists = Artist::with('songs')->take(500)->get();

$artist->songs;

Eager loaded with only songs that made top 10:

$artists = Artist::with('songs' => function ($query) {
    $query->where('highest_position', '<=', 10); 
})->take(500)->get();

$artists->songs;

Consider the following loop. If we don’t eager load the songs then for each artist a query to the database will be executed. This example may be a bit over the top because in most cases paginating the data would also solve part of the issue in this specific example, but I wanted to outline the potential issue.

foreach ($artists as $artist) {
    $artist->songs->each(function ($song) use ($artist) {
        $song->fullName = $artist . ' - ' . $song->title;
    });
}