April 15, 2020
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.
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:
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.
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.
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.
Let’s use a simple system as an example. Consider an
Article model, which has a child Comment model (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
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:
First, add the option to the child model (in this case, the Comment). Edit the child model file and modify the
Add a column to the parent database table (
articles) by creating and running a migration.
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.
Finally, access the
counter_cache value in the application’s views (or any other display component) by using
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:
article.comments.countmethod, which always performs a
article.comments.lengthmethod, which loads all the child comments into memory (unless they’ve been previously loaded) and then gets the length of the array.
article.comments.sizemethod without using
counter_cache, which queries the database unless the comments array is already in memory, having been previously loaded from the database by another code path.
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
$ 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 %>
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
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.
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.