Skip to main content

Rails Counter Caches Discussion

General • Asked by Chris Oliver

I feel this video was cause of me in the forum haha!

A+++

Haha! It just might be. :) Someone was also asking about counter caches in Slack so it seems like perfect timing!


I was just working on something like this today; awesome timing! One pain point I've run into is conditional counter caching. Rails doesn't have it built-in, so I have to add some callbacks that run the counts myself.

Would be great to see a follow-up video on optimizing conditional counts. I know that doing a raw count() works for 99% of the cases, but for special race conditions and possible performance it may be best to use the SQL coalesce syntax (what Rails uses for counter_cache I think). I'm just not well-versed enough in SQL to be able to write it without a little research.

As an example of what I currently do, I have a situation where a User has many Tasks, but I only care about counting active tasks:

# models/task.rb
after_save :update_user_active_tasks_count
after_destroy :update_user_active_tasks_count

def update_user_active_tasks_count
user.update_column(:active_tasks_count, user.tasks.active.count) if user
end

This seems very inefficient to run on every task update.

This is exactly what the counter_cache option does inside ActiveRecord, it's just hidden away. You get the performance benefit from querying, with the slight tradeoff of inserting/destroying records taking 1ms or so more. That's totally fine because most applications are much more read heavy than write heavy.

As for the conditional counter caching, you're right, nothing in Rails to do that. You could probably turn that into a concern that could be reusable that accepts a proc you call to replace the `if user` portion so it could be reused and customizable in any fashion. Might even make for a nice little gem!

Another tweak you could do with that is actually do the COALESCE like you mentioned, but like Rails does, ignore the count and actually just add 1 or subtract 1 from the current value. This way you don't have to also query for the total count which would speed up your implementation slightly. It does assume the value is accurate, which is totally fine since like we saw in the episode that you can just set that when you add the new column. This way you're making a micro update to just increment or decrement the counter column rather than doing that plus selecting the count. Adding or subtracting 1 each time should only ever take a millisecond or less while the count might take several.

This was a great question!


Hi Chris,

I'd like to share a few thoughts with you.

Currently, there are plenty of tutorials and learning materials for junior to mid-level developers on various topics. However what I think is lacking is training materials on more advanced topics.

Take this episode for example. This episode is of very high quality, you explain how counter caching works very well. I think you have covered most of the important aspects of the basics. The thing is, there is already plenty of good materials out there, which cover the same. I think it would be really nice to have a more in-depth look at counter caching. For example:
* how do you count has_many :through associations;
* how do you count scoped has_many associations `article has_many :public_comments, -> { where(public: true) }, class_name: 'Comment'`;
* what about soft deleting - you don't actually destroy the child object, but just set its `deleted` attribute to `false` - how do you handle the restoring of the object (setting `deleted` back to `true`;
* what are the gotchas of implementing a counter cache on your own;

I think if you combine all those conditions, you can come up with some pretty challenging scenarios.

I know that it doesn't make much sense to jump right into the complicated stuff without covering the basics first, but I really wish to see more advanced-level stuff in the pro episodes.

Thank you and keep up the good work.

Second that, I hope Chris will make another episode. Very nice idea (y)

I mean to reply last week but lost the browser tab. :)

Absolutely love these suggestions and I'm going to record a video on advanced counter caches for sure. Please keep the suggestions coming, these are all fantastic and a welcome addition to all the beginner screencasts I often get requests for.

Here's the latest episode on advanced counter caching. 🙌 https://gorails.com/episode...


I noticed that the user model had to be reloaded to decrement but not to increment in a spec and the rails console in a version as late as 4.2.7.1
.

describe "counter caches" do
it "increments associations when created" do
user = User.create(first_name: "David", last_name: "Moore", email:"[email protected]")
user.forum_threads.create(name:"First thread")

expect(user.forum_threads_count).to eq 1 #passes

ForumThread.create(name: "Second thread", user: user)

expect(user.forum_threads_count).to eq 2 #passes

#destroy a forum thread and check if form_threads_count is updated
ForumThread.last.destroy

expect(user.forum_threads_count).to eq 1 # fails
expect(user.reload.forum_threads_count).to eq 1 # passes
end

That's probably because when you do the destroy, Rails has no idea you have a local variable called "user" and therefore it can't update it to keep it in sync.

That makes sense. Just an interesting observation. Great post btw.



Thank you for this episode Chris.
The last explanation about using forum_posts_count instead of forum_posts.count is priceless :)


Nice episode, thank you for sharing. Can be apply the same for unread message? 

Great episode Chris thanks for this video that's prevents lot of count() queries


Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 24,647+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.