What's a counter cache?

Jose Blanco

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!