N+1 queries are not always a problem, but I have seen that most of the n+1 queries that are really a problem are when we need to fetch data to compute something.
Here I will try to share some examples of posible expensive computations candidates to be cached and some patterns that you could use to save different kind of values.
I think the most common computation in many apps will be a count. It is that common that rails already have “counter caches”.
But sometimes you will need to save counts where a counter cache won’t be enough, like:
Or maybe other examples where you need other type of aggregation like:
No, not really, you should evaluate the pros and cons in each situation.
Sometimes you will need a way to cache the value since the first implementation, but other times it will be better to wait and learn a little more about the problem and cache the value until is really necessary.
Sometimes pre-compute the value will be very easy, but sometimes could be more expensive or very hard, or maybe just not needed.
There are many ways to save a value, but here I will show you three patterns that I use a lot.
You could use it for calculations that involve information on the same record, like a profile completeness calculation.
class Profile < ApplicationRecord
before_save :set_completeness_percentage
def set_completeness_percentage
self.completeness_percentage = calculate_completeness_percentage
end
end
When you use a before_save
callback, you don’t need to save the record, you just need to change the value and the record will be saved including the fields you modified.
You could use it when you need to save or change a value for things that happen in other records, like updating the balance when an entry is created or updated:
class Account < ApplicationRecord
has_many :entries
after_touch :update_balance
def update_balance
update(balance: entries.sum(:amount))
end
end
class Entry < ApplicationRecord
belongs_to :account, touch: true
end
Use it for calculations are expensive to run on the same process and is better to run them asynchronously.
class Profile < ApplicationRecord
after_commit :set_completeness_percentage_later
def set_completeness_percentage_later
Profile::SetCompletenessPercentageProfileJob.perform_later(self)
end
def set_completeness_percentage
self.completeness_percentage = calculate_completeness_percentage
end
end
class Profile::SetCompletenessPercentageProfileJob < ApplicationJob
def perform(profile)
profile.set_completeness_percentage
profile.save!
end
end
When saving computed values in the database in your rails app, you must be aware that is possible to find unexpected errors in the result thanks to race conditions.
I have other article that can help you visualize how race conditions can make you save incorrect values even when your calculation is correct.
You can read it on: Saving incorrect computed values thanks to race conditions
Learn just enough fundamentals to be fluent preloading associations with ActiveRecord, and start helping your team to avoid n+1 queries on production.