Using Laravel’s Eloquent Efficiently

Rudi Theunissen
codeburst
Published in
4 min readJun 20, 2017

--

Ever since Laravel 5.3, I’ve seen many cases where Eloquent’s relations and their underlying query builders are not used efficiently. Many of these cases exist because there are multiple paths to the same data, many of which look and feel like equivalent solutions. Unless you are familiar with what’s going on under the hood, it’s not always clear which path would be the better one.

The decision to make is usually between $blog->posts and $blog->posts().

The first will go to the database, fetch all posts related to the blog, and create a Collection of Post models. This is expensive because we have to create and hydrate an instance of every related model.

A good question to ask yourself is “do I need an instance of the model?”. It’s common to only need data for a few attributes. Unless you’re relying on attribute casting or mutators (Carbon dates for example), you can get these attributes directly from the database.

$blog->posts() will return a Builder straight away, without making any queries yet. The question to ask here is “do I need the entire collection?”. Can the query be more specific? A good policy is to do as much as possible in the database during the query and as little as possible in PHP, for example a where in the database will be much faster than where on a Collection.

Let’s take a look at some examples.

Don’t use a collection to count the number of related entries.

$blog->posts->count() will create a collection of Post models, along with all their attributes, and return the number of models in the collection. If the posts relation is already loaded on the model then this will actually be the faster path because you wouldn’t need to make any queries at all.

$blog->posts()->count() will execute a single aggregation query and get an integer back from the database. No models, no attributes. We just want the number of items as an integer, right?

Casual benchmarks indicate that the second one is ~20 times faster for a collection of 5,000 items, but this will be different on other environments so it’s worth testing this to see for yourself. Note that the the difference will become more extreme as the size of the collection gets larger.

Don’t fetch the entire collection if you only need the first model.

This might seem like an obvious mistake but I‘ve seen it enough to mention.
$blog->posts->first(), which like before has to fetch and hydrate an entire collection of related models only to return the first and discard the rest.

If you only want the first model you should use the relation’s query builder: $blog->posts()->first(). This will set the proper OFFSET and LIMIT when performing the query, returning and hydrating only a single model.

Wait so hold on, how does that work? The key to understanding this is to realise that some methods called on the relation (or the model) pass through to the query builder via a magic __call on those classes. So the first() call on the posts() relation actually ends up calling first() on a Builder that is created using the model or related model’s table name.

Don’t use “where” on the collection if you can do so on the builder.

$blog->posts->where(‘author’, 1) will create all the models first, then iterate through the collection one by one to test against the author, creating a new separate collection, and finally returning the number of items in that collection. Any indexes we have in our database are useless because we’re inspecting the models in PHP.

$blog->posts()->where(‘author’, 1)->get() is all internal to the database, which can make use of indexes and only returns the models that match the where, so we don’t have to hydrate them all when we process the results.

Don’t use “pluck” on the collection, use the builder instead.

Laravel projects use pluck everywhere. We often just want a single column’s value, sometimes keyed by another, for example a list of user names keyed by the user’s id. If we use our blog example, we might want the name of every post keyed by the post_id.

One way to achieve this is to use $blog->posts->pluck('name', 'id'), but we are creating a collection again, and to do that we have to create all the models as well. We don’t need the models though, we just need a tiny slice of their attribute data, and we can use the builder for that.

$blog->posts()->pluck('name', 'id') might look very similar but the behaviour under the hood is very different. The pluck call passes through to the builder, which does a SELECT name, id on the model’s table, and builds the array using the raw results from the database.

As mentioned earlier, using the builder will not call any of the model’s mutators, because it’s simply not aware of the Eloquent model at all. In the example above, if name was a dynamic attribute or modified on access using getNameAttribute on the model, pluck on the builder will be different.

There are many more cases like these where using the builder directly can make a big difference. Always consider what the framework is doing behind the scenes, and if it’s not clear, step into the source and follow it through.

--

--