How to Start Using Counter Caches in Rails

It is widespread to have parent-child associations in Rails applications. On the parent side is a :has_many association, and on the child side is a :belongs_to association. Examples include an article with comments, or an author with books--the former is the parent, and the latter is the child. 

It is often useful to display a count of the children alongside information about the parent, without necessarily loading all the child records. For example, if an application has articles with comments, users looking at an article list will want to know which articles have the most comments. 

Using the counter_cache in this situation will result in fewer requests to the database and, therefore, generally better performance.

Use these links to skip ahead in the guide:

Active Record’s Counter Cache

Getting Started

Counter Cache Examples

Counter Cache Gotchas

Rails Counter Cache Explained

The counter cache may seem mysterious, but it’s not that complicated. It’s just a database column storing the number of children, with the value automatically updated. 

Counter Cache Definition

For any parent model with an associated child model, the counter_cache is an integer database column with the value maintained by the Rails framework. An application should treat it as read-only. The column contains a pre-calculated number of associated children. 

Note that the counter_cache is different from many of the other Rails caches, which have specialized stores (such as the filesystem, memory, an external key-value store). The value of the counter_cache, on the other hand, is stored in the database. The benefit is that the application almost never needs to query the child database table for the count of children.

How Do Counter Caches Work?

Whenever any code in a Rails application changes the number of children of a parent model, the value of the counter_cache column is automatically updated. So when the number of associated child models increases (for example, a comment is added to an article), the counter_cache automatically increments. When the number of associated child models decreases (a comment is removed from an article, perhaps through admin moderation), the counter_cache automatically decrements.

When displaying the parent, no additional database queries are required to get the number of children. The count of children is already present in the parent object.

Getting Started

Let’s use a simple system as an example. Consider an Article model, which has a child Comment model (the Comment:belongs_to the Article and the Article:has_many Comments). When the list of Articles is displayed, the system should also show the number of Comments. We expect a lot of user-submitted comments for some articles and a few for others. We also know that this is a read-heavy site, so we want to reduce the number of database queries. 

This is a perfect place to use the counter_cache.

Note that this is a classical trade-off between storage space and time. The Article model will take slightly more space (because it has a new column with the number of children), but by pre-calculating the number of children at Comment creation time, we decrease the time needed to display that value.

Follow along step-by-step as we learn how to use counter caches:

Set up the child model

First, add the option to the child model (in this case, the Comment). Edit the child model file and modify the :belongs_to section.

Set up the parent table

Add a column to the parent database table (articles) by creating and running a migration.

Reset the counter cache (optional)

Sometimes, the counter_cache value for existing records will need to be updated. When we add the column to the articles table, it will have the default value (typically 0). This is fine if the system has no articles, but the counter_cache is being added to help improve performance, make sure that all existing articles are updated with the number of their comments. 

Display the number of children

Finally, access the counter_cache value in the application’s views (or any other display component) by using article.comments.size

When we use this, Rails won’t query the comments table, unlike other methods of finding the count of the children. Some other methods to find the number of comments include:

Counter Caching Example

Let’s examine some code! We’ll use the same models and relationships we walked through in the Getting Started section. As a reminder, we have an Article model (the parent) that has many Comments (the children), and each Comment belongs to one and only one Article. 

First, we need to make sure we have our relationship set up correctly (without using the counter_cache):

$ cat article.rb

class Article < ApplicationRecord

  has_many :comments, dependent: :destroy

end

$ cat comment.rb

class Comment < ApplicationRecord

   belongs_to :article

end

Next, add the counter_cache option to the Comment model (add the text in bold):

$ cat comment.rb

class Comment < ApplicationRecord

  belongs_to :article, counter_cache: true

end

Then, we need to create and run a migration that will add the column for the cached count:

$ rails g migration AddCommentsCountToArticles comments_count:integer

This creates this migration: 

class AddCommentsCountToArticles < ActiveRecord::Migration[6.0]

  def change

 add_column :articles, :comments_count, :integer

  end

end

Run the migration:

$ rails db:migrate

Note that parent table column name (comments_count) typically matches the naming convention of <child_table_name>_count . If, for some reason (legacy compatibility, perhaps), we need a different column name, specify a different column name in the model file. 

To use the total_comments column as the counter cache, this is what the:belongs_to section of the Comment model would look like:

belongs_to :article, counter_cache: :total_comments

Next, we want to update the column value for existing records. If there are no existing Articles, skip this step. We want to do this across all environments, which is why we do this via a migration.

$ rails g migration ResetAllArticleCacheCounters

Next, update the generated migration file to this:

class ResetAllArticleCacheCounters < ActiveRecord::Migration[6.0]

  def up

 Article.all.each do |article|

     Article.reset_counters(article.id, :comments)

     end

  end

  def down

     # no rollback needed

  end

end

Run our migrations:

$ rails db:migrate

Finally, we can access the child count in a view file, and the value of the counter_cache column will be displayed, and the comments table will not be queried:

<%= article.comments.size %>

Gotchas

If the application adds or removes child records but doesn’t use Rails, the counter_cache column won’t be updated--this might occur with an external data loading process. If this is the case, the process can run code similar to the ResetAllArticleCacheCounters migration at the end of each data load. 

The counter cache by default decrements when a child model object is destroyed. If the application uses a soft delete gem such as paranoia, the counter cache may or may not be updated. Make sure to review the relevant gem’s documentation to verify compatibility.

A given model can have multiple counter caches. For example, an article might have a counter_cache for comments and a counter_cache for citations.

Never modify the counter_cache column from the Rails code. It should only be modified by the reset_counters method. 

As mentioned above, when using the counter cache, you are introducing some complexity, just like any other cache. Adding or removing children is going to take slightly longer, and the parent model will be slightly larger. That’s the trade-off to have the number of children stored on the parent model. As always, examine real-world usage and benchmark it to make sure the correct trade-offs are made.

Conclusion

The counter cache is a useful Rails framework feature. If an application has a parent-child association and renders, the parent’s child count often, a counter cache is an easy and performant way to do so.