In Rails applications, N+1 queries can cause significant performance issues. They occur when you have a list of items and perform a separate query to retrieve associated data for each item. This can result in poor performance, and even more so if the list contains a large number of items.
A problem
I worked on an application, and initially, I wrote the following index
method with a list of items and for each item, it make a separate query to retrieve associated data, as you can see this method represents an example of vulnerable code to the N+1 query problem:
def index
@companies = Company.most_recent
@open_companies = Company.with_active_status.includes(:_members, :users).most_recent
@previous_companies = Company.without_active_status.includes(:_members, :users).most_recent.limit(50)
@archived_companies = Company.with_archived_status.includes(:_members, :users).most_recent
end
Here we have separate queries are run for each company
to retrieve the associated _members
, users
, and user
needed data. This can result in poor performance, not to mention if a large number of companies are displayed.
I need to avoid the N+1 query issue. How? Eager loading can be used to include the associated data in the initial query. In Active Record, this is accomplished by using the includes
method.
A Solution
Here is a modified version of the index method that avoids the N+1 query problem by using the includes
method:
def index
# Eager load _members, users, and user associations for @companies
@companies = Company.includes(:_members, :users, _members: [:user]).most_recent
# Eager load _members and users associations for @open_companies
@open_companies = Company.with_active_status.includes(:_members, :users).most_recent
# Eager load _members and users associations for @previous_companies
@previous_companies = Company.without_active_status.includes(:_members, :users).most_recent.limit(50)
# Eager load _members and users associations for @archived_companies
@archived_companies = Company.with_archived_status.includes(:_members, :users).most_recent
end
That’s much better as you can tell. The new index
method employs the includes
method to eagerly load the associations for each of the variables @companies
, @open_companies
, @previous_companies
, and @archived_companies
. This reduces the number of queries made and improves code performance, assisting in avoiding the N+1 query issue.
Using eager loading can improve the performance of your application and avoid the N+1 query issue.
You can improve the performance of your application and avoid the N+1 query issue by using eager loading. Use it as much as possible to keep your code running smoothly and efficiently.
Refactor
While we solved the N+1 issue, the code smells because it breaks the DRY principle of software development. And we can avoid that by refactoring the ‘index’ method.
The refactored method below, combines the queries for the variables @open_companies
, @previous_companies
, and @archived_companies
into a single query, reducing the number of queries and improving overall code performance as we continue to use the includes
method, isn’t that beautiful:
def index
# Eager load _members, users, and user associations for @companies
@companies = Company.includes(:_members, :users, _members: [:user]).most_recent
# Eager load _members and users associations for @open_companies, @previous_companies, and @archived_companies
companies = Company.includes(:_members, :users)
# Filter companies by active, inactive, and archived status
@open_companies = companies.with_active_status.most_recent
@previous_companies = companies.without_active_status.most_recent.limit(50)
@archived_companies = companies.with_archived_status.most_recent
end
The last three instant methods call the Company object that includes _members
attribute three times. And this could be extracted into a variable to call different scopes.
As you have seen, following the DRY principle and using Eager Load can improve the performance of your application and avoid the N+1 query issues. Be sure to use it whenever possible to keep your code running smoothly and efficiently.
And as usual, Happy Coding 😀 💻