Nowadays users of modern web and mobile applications expect a fast response for each interaction. As developers, we need to be aware of the performance of our applications and how we can improve it. This involves constantly searching for potential bottlenecks, and especially looking for inefficient queries when we are dealing with large amounts of data.
One of the most common performance issues in Rails applications is the N+1 query problem. This problem occurs when we are loading a collection of records and for each record we are running a separate query to count how many records are associated with that record.
A common example
Imagine that your web application is about keeping track of how many comic books you have in your collection.
Each collection has_many :commic_books
and each comic book belongs to a different Genre
like superhero,
manga, mystery, horror, and more.
Now you want to show to the user how many comic books are in each genre.
Understanding Counter Caches
Continuing with the previous example a naïve approach might look something like this:
class Genre < ApplicationRecord
has_many :comic_books
def comic_books_count
comic_books.count
end
end
Somewhere in our application:
<% @genres.each do |genre| %>
<%= genre.comic_books_count %>
<% end %>
This will lead to an N+1 query problem. For each genre we will run a separate query to count how many comic books are associated with that genre and this is not efficient.
The below will be the SQL query generated by Active Record:
SELECT "genres".* FROM "genres"
SELECT COUNT(*) FROM "comic_books" WHERE "comic_books"."genre_id" = $1 [["genre_id", 1]]
SELECT COUNT(*) FROM "comic_books" WHERE "comic_books"."genre_id" = $1 [["genre_id", 2]]
As you can see we are running one query for each comic book genre. This is not efficient and it will slow down our application.
Counter Cache to the rescue
Rails provides a way to cache the number of associated records
in a separate column using the counter_cache
option.
class ComicBook < ApplicationRecord
belongs_to :genre, counter_cache: true
end
class Genre < ApplicationRecord
has_many :comic_books
end
To do this, we can just add a new column to the genres
table named comic_books_count
like this:
class AddComicBooksCountToGenres < ActiveRecord::Migration[7.0]
def change
add_column :genres, :comic_books_count, :integer, default: 0, null: false
end
end
Rails takes care of updating the comic_books_count
for you. When a comic book is associated with a genre, it will
increment, and when a comic book record is deleted, it will decrement automatically and everything is
handled efficiently by Rails.
By setting a default value of 0, we can avoid a nil
value in the database. This is important because it provides a
starting point for those genres that don’t have any associated comic books yet.
To display the number of comic books we can just do:
<% @genres.each do |genre| %>
<%= genre.comic_books_count %>
<% end %>
The SQL query generated by ActiveRecord will be:
SELECT "genres".* FROM "genres";
Mission accomplished! 🎉 Because each genre knows how many comic books are associated with it, Active Record won’t run a separate query for each genre.
Conclusion
Rails provides simple and efficient solutions to complex issues. The counter cache option is one of many. I hope this post will help you to improve the performance of your rails applications!