Speed up relationship queries in Laravel

Adding indexes to your database tables is a great way to get some extra performance out of your application, especially if you have a large amount of data in your tables. They should be used sparingly and only on identified slow queries, as they have implications of their own such as increased table size and increased RAM usage. But those potential drawbacks are well worth it when you can get a query down from 3 seconds to 15 milliseconds with 5 minutes of work. The effects are particularly noticeable on polymorphic / many to many polymorphic relationships.

The type of index created depends largely on the relationship type and the content structure.

Creating indexes in Laravel

Adding indexes to your tables in Laravel is very straightforward. In your migrations you can add:

// Single index
$table->index('video_id');

// Compound index
$table->index(['commentable_type', 'commentable_id']);

See the Laravel documentation on indexes for more info.

One to one / one to many relationships

One to one and one to many relationships primarily work through foreign keys. Say we have 2 tables, users and addresses, where each user can have one address (one to one), the foreign key would be the user_id column on the addresses table. The same can be said for if a user can have multiple addresses (one to many).

For each of your foreign keys in your migration, you should let your database know that they are indeed foreign keys:

$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');;

You also get some extra benefits out of this by using foreign key constraints which will ensure data integrity, i.e. making sure you don't delete a profile which is being used by a user.

By default, MySQL using the InnoDB storage engine will add an index automatically for foreign key fields, but if you're using a different set up, you can add one like this:

$table->index('user_id');

Many to many relationships

Many to many relationships require an extra table, known as a pivot table. Let's use the example from the Laravel docs of many users having many roles, where many users could share the role of administrator. Our table set up for this example would be something like this:

// users table
$table->increments('id');

// roles table
$table->increments('id');

// role_user pivot table
$table->unsignedInteger('user_id');
$table->unsignedInteger('role_id');

As in the one to one relationship example above, we can use foreign keys to get all of the same benefits, but this time we can add two to our pivot table:

// role_user pivot table
$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->unsignedInteger('role_id');
$table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');

Bonus

As a bonus, if you want to make sure that a duplicate can never exist in your database for a combination of the user and role, you can add a unique constraint:

$table->unique(['user_id', 'role_id']);

Polymorphic / many to many polymorphic relationships

Indexes really come into their own on the types of queries required for polymorphic relationships. I have seen queries that previously took 30 seconds to run to go all the way down to 100 milliseconds.

Let's start with another example. Our application this time has videos and posts, and each of them can have many comments. This is a polymorphic relationship: one post or video can have many comments. Our table set up would look like this:

// videos table
$table->increments('id');

// posts table
$table->increments('id');

// comments table
$table->increments('id');
$table->text('comment_body');
$table->text('commentable_type');
$table->unsignedInteger('commentable_id');

Our index for this example is a bit different than the previous examples. Because almost every query we make on the comments table will include both the commentable_id and the commentable_type we need to create an index on the pair of them, this is called a compound index. To create this index we can pass two column names into the index method as an array.

// comments table
$table->index(['commentable_id', 'commentable_type']);

The same index would apply if you were to use a many to many polymorphic relationship too, only you would apply it to your pivot table.

Order of fields in compound indexes

In terms of the performance, the order of the fields in the compound index does matter. The rule of thumb is to order them by whichever narrows down the results the most first. For example, given the following table:

commentable_id commentable_type
1 App\Video
2 App\Video
3 App\Video
1 App\Post
2 App\Post
3 App\Post

Say we want to run a query to get the comments for a Video with the id 1.

If we were to create an index using $table->index(['commentable_type', 'commentable_id']);, the index will first filter on commentable_type, which would reduce the results to 3 items:

commentable_id commentable_type
1 App\Video
2 App\Video
3 App\Video

And then by commentable_id, which would reduce our set to 1:

commentable_id commentable_type
1 App\Video

Conversely, if we create our index using $table->index(['commentable_id', 'commentable_type']);, the index will first filter on commentable_id, which would reduce the results to 2 items:

commentable_id commentable_type
1 App\Video
1 App\Post

And then by commentable_type, which would reduce our set to 1 again, getting the same result.

This contrived example demonstrates that filtering by commentable_id first makes the second filter - by commentable_type - quicker because it has fewer items to look through.

The order you choose completely depends on your content. If you're likely to have few comments on many commentable items then it's going to be better to filter on commentable_type first. But, as in our example, if you have few commentable items, but many comments on that item then it's going to be better to filter on commentable_id first.

Don't worry though, whichever you choose is going to be a performance win over not having an index at all.

Further

If you'd like to learn more about indexes, I highly recommend the Performant Laravel course on Servers for Hackers. It's free and includes 3 videos on indexing in MySQL as well a bunch of other useful performance tips.