Benito Serna Tips and tools for Ruby on Rails developers

4 things you can try before using a counter cache in rails

May 16, 2022

Maybe you have heard about the counter cache feature.

A counter cache makes finding the number of belonging objects more efficient by keeping a column with the count.

Rails makes it easy to implement it, but is not free. Sometimes it may be better not to use it.

Here are four things you could try before or instead of using a counter cache, without introuducing n+1 queries.

1. If you don’t need the count in a list just count the records

If you don’t need to use the count inside a list, and instead you just want to show the count of the association in a details or stats page, you can try to count the records via SQL using count or size.

post = Post.first
# Post Load ...
post.comments.size
# Comment Count (0.4ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 1]]

# or...
post.comments.count
# Comment Count (0.4ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 1]]

If you need to load all the records in the association in the same page and without pagination, you can load the records and count the records with ruby using length or load.size, to make just one call to the database instead of two.

post = Post.first
# Post Load ...

# Length will load the coments and count the elements in ruby
# avoiding one extra database call.
post.comments.length
# Comment Load (0.2ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 1]]

# or...
# #load will load the coments and #size will count the elements
# in ruby avoiding one extra database call.
post.comments.load.size
# Comment Load (0.2ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 1]]

If you have doubt when is better to use count vs size vs length you can read the post: Difference between count, length and size in an association with ActiveRecord.

2. Preload the records in a list

If you need to display the count of an association for each record in a list, and you know the records wont’t have too many associated records, you can try to preload the records and use lenght or size to count the records in ruby.

# Preload the full association
posts.preload(:comments).each do |post|

  # Use size to count with ruby
  post.comments.size

  # or lenght
  post.comments.length
end

But be carefull, because if the association has too many records, you can introduce a performance problem.

3. Preload the counts you’ll need in a list

If you need to display the count of an association for each record in a list, and you know the records will have too many associated records, as I said before, preloading all the associated records can introduce a performance problem.

Instead you can try to preload just the counts with something like this:

posts = Post.limit(5)
# Filter the likes for those posts and group them by count
likes = Like.where(post_id: posts).group(:post_id).count

posts.each do |post|
  # Find the likes count for the current post
  # and fallback to zero if no count
  puts "Post: #{post.id}, likes: #{likes[post.id] || 0}"
end

If you want to see other ways to preload the counts, you can see the post: How to preload counts in a list with ActiveRecord.

4. Fragment caching

If you can enable caching in your project you can also try to avoid the n+1 queries in the list with fragment caching.

You can think of n+1 as a feature, and let the n+1 queries run just the first time and use the cache after that.

For example if you have a posts/_post partial like this:

<p><%= post.abstract %></p>
<span>Likes: <%= posts.likes.size %></span>

And then in the index view, you call the partial with something like this:

<%= render partial: 'posts/post', collection: posts, cached: true %>

You will have n+1 queries on the first render, but after that, rails will read the cache instead avoiding the n+1 queries.

To learn more about rails caching you can go to: Caching with Rails: An Overview

Warning: Don’t try too hard to avoid the counter cache

Although sometimes you can avoid the counter cache with these tactics, sometimes these tactics can also give you some troubles.

So take this ideas as suggestions, but don’t think that one of this will solve all your problems every time.

Take them as options to help you solve your current problem.

Related articles

No more… “Why active record is ignoring my includes?”

Get for free the first part of the ebook Fix n+1 queries on Rails that will help you:

  • Explain what is an n+1 queries problem
  • Identify when ActiveRecord will execute a query
  • Solve the latest comment example
  • Detect n+1 queries by watching the logs
  • Learn the tools to detect n+1 queries
Get the first part of the ebook for free