Development Blog
Eloquent Performance
To require this package via composer as a dev dependency because we only need this locally and we NEVER want this running in production:
composer require barryvdh/laravel-debugbar --dev
Laravel uses Package Auto-Discovery, so doesn't require you to manually add the ServiceProvider.
The Debug bar will be enabled when APP_DEBUG is true. In your .env file. When enabled you’ll see the following in your browser:
The performance metrics
Page Load
Queries
The Queries tab is a great way to identify N+1 issues as it shows the total number of queries (N+1 issues are not scalable, the more data you have, the more its going to impact performance, you’re executing an extra query per row of data).
It might identify areas of your code where you could utilise eager loading.
In the screenshot below, we’re selecting users, which takes 1.61ms. But what if we needed to order them by their name?
Notice how much longer it takes? It’s jumped from 1.61ms to 28.11ms.
This might be an indication that a database index is required on the ‘name’ column. After adding the index we get a query time of 2.4ms - much faster than 28.11ms!
Memory
The models tab comes in handy here! It shows how many models have been loaded! In the example below, you can see we have 16 company and 16 user models loaded.
But what happens if we eager load users from within the company model?
You may notice a memory increase (in this example it increases by 2MB), and we’re loading a lot more user models! This change has had quite a negative impact on the application.
Take this example, the memory usage is at 19.8MB:
This is because we’re pulling all the content from the posts table, which includes the post content (which is quite large), since this page doesn’t need to know the post content for all posts (its listing posts with a link to the individual post page), why pull all the data?
If we update the query to pull the columns we need, this happens:
->select('id', 'title', 'slug', 'author_id')
We’re down to 4.35MB of memory usage and we’ve shaved the page load slightly too. You could also apply to this any eager loaded models too. In this example we only need the author id and name, I.e:
Changing:
->with('author')
To:
->with('author:id,name')
Will improve memory usage too.
Assuming you have a logins relationship within your user model, you could add this into your view (in a last login column), it might look something like this:
{{ $user->logins()->latest()->first()->created_at->diffForHumans() }}
In the browser, you’d get the following:
However, in the debug bar, there’s a problem - for every user we display we’re executing an additional query to get their last login (N+1 issue). What if the page displayed 50 users, or 1,000 users?
If we eager load ‘logins’.
->with('logins')
And adjust the view
{{ $user->logins->sortByDesc('created_at')->first()->diffForHumans() }}
This reduces the total amount of queries (from 17 to 3):
However, we’ve introduced another problem - see the number of models above? It’s into the thousands now and the memory usage is going to increase! Eager loading ALL this data is not a better solution.
Before we fallback to caching (Don’t be that guy, or at least use it as a last resort!?), potentially (in this scenario) a sub query could be useful. Using addSelect might be the answer.
->addSelect(['last_login_at' => Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1);
])
->withCasts(['last_login_at', 'datetime'])
It makes logic more expressive throughout your application and it's available for reuse.
And you’d change the view to:
{{ $user->last_login_at->diffForHumans() }}
This was the situation before:
Now it looks like this:
You could copy and paste what we’ve already done and rename a few bits, but is there a better way? This approach isn’t really scalable. We could end up with loads of query scopes!
Create a relationship and a scope.
public function lastLogin()
{
return $this->belongsTo(Login::class);
}
public function scopeWithLastLogin($query)
{
$query->addSelect(['last_login_id' => Login::select('id')
->whereColumn('user_id', 'users.id')
->latest()
->take(1);
])->lastLogin();
}
You’d simply query the model via:
->withLastLogin()
And the view would be updated to the following:
{{ $user->lastLogin->ip_address; }}
{{ $user->lastLogin->created_at->diffForHumans(); }}
You can’t lazy load dynamic relationships as no ‘last_login_id’ will be present on the model, as this depends upon the WithLastLogin scope.
And finally... you might think that using the relationship below, would surely work?
public function lastLogin()
{
return $this->hasOne(Login::class)->latest();
}
Unfortunately not, it reintroduces the N+1 problem...