Optimizing Ruby on Rails Applications with Counter Cache

Ruby on Rails offers a useful feature called Counter Cache that refers to a column in the database that keeps track of how many associated records a model has. It’s particularly useful in scenarios where we frequently need to retrieve the count of associated records without incurring the overhead of querying the database each time.

Luckily for us, Rails automates the maintenance of this counter, ensuring that it stays synchronized with the actual count of associated records.

For this post, let’s assume we have a Tweet model that has many Likes(sidenote: Did you know Twitter was built using Ruby on Rails? 🙃)

Without Counter Cache

Suppose we need to iterate over all the tweets and count how many likes each tweet has. We would have something like this:

tweets = Tweet.all
likes_count_without_cache = tweets.map { |tweet| tweet.likes.count }

In the code above we are generating, for each tweet, a query to the database to grab the number of likes it has. This doesn’t look good in terms of performance, especially when you have many tweets.

Now, let’s see how we can improve that with Counter Cache.

Setting Up Counter Cache Columns

First, we need to add a counter cache column to the tweets table:

# Migration to add counter cache column to tweets table
class AddLikesCountToTweets < ActiveRecord::Migration[7.0]
def change
add_column :tweets, :likes_count, :integer, default: 0
end
end

After running this migration, the Tweet model needs to be updated to reflect the counter cache association.

# Tweet model
class Tweet < ApplicationRecord
has_many :likes
end

# Like model
class Like < ApplicationRecord
belongs_to :tweet, counter_cache: true
end

Now, every time a new tweet is liked (or a like is removed from it), Rails will automatically update the likes_count column in the corresponding tweet.

Important

Keep in mind that, if there are tweets already in the database, you will need to manually calculate the likes_count for those existing records:

# Manually resetting the counters for the existing records
Tweet.find_each do |tweet|
Tweet.reset_counters(tweet.id, :likes)
end

Counter Cache in Action

Instead of executing a separate query to count how many likes a tweet has, Rails can retrieve the value stored in the likes_count column, improving our app’s performance. Going back to the example we used before:

tweets = Tweet.all
likes_count_with_cache = tweets.map(&:likes_count)

The .map method will generate only one query to get all counts. This not only reduces the number of database queries but also improves the overall performance of your application, especially when dealing with large datasets.

Conclusion

In conclusion, Ruby on Rails counter cache columns provide a convenient and efficient way to manage the count of associated records. Integrating counter cache columns is a simple yet powerful optimization technique that can significantly impact the efficiency of your Rails application.