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.
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.
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.
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.
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
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.
Learn just enough fundamentals to be fluent preloading associations with ActiveRecord, and start helping your team to avoid n+1 queries on production.