I’m trying to optimise some N+1 queries in active record for the first time. There are 3 to kill – 2 went very easily with a .includes call, but I can’t for the life of me figure out why the third is still calling a bunch of queries. Relevant code below – if anyone has any suggestions, I’d be really appreciative.
CONTROLLER:
@enquiries = Comment.includes(:children).faqs_for_project(@project)
MODEL;
def self.faqs_for_project(project)
Comment.for_project_and_enquiries(project, project.enquiries).where(:published => true).order("created_at DESC")
end
(and the relevant scope)
scope :for_project_and_enquiries, lambda{|p, qs| where('(commentable_type = ? and commentable_id = ?) or (commentable_type = ? and commentable_id IN (?))', "Project", p.id, "Enquiry", qs.collect{|q| q.id})}
VIEW:
...
= render :partial => 'comments/comment', :collection => @enquries
...
(and that offending line in the partial)
...
= 'Read by ' + pluralize(comment.acknowledgers.count, 'lead')
...
Two SQL queries are called for each comment. The 2 queries are:
SQL (2.8ms) SELECT COUNT(*) FROM "users" INNER JOIN "acknowledgements" ON "users".id = "acknowledgements".user_id WHERE (("acknowledgements".feedback_type = 'Comment') AND ("acknowledgements".feedback_id = 177621))
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1295 LIMIT 1
I would have thought appending (:user, :acknowledgements) into the controller’s .includes would have solved the problem, but it doesn’t seem to have any effect. If anyone has any suggestions on what I’m missing, I’d be really appreciative
I believe in your
Commenttable you want to add a:acknowledgers_countcolumn as a counter cacheYou will need to create a migration to add the
:acknowledgers_countcolumn to thecommentstable. Rails should take care of the rest.You can learn more about the
ActiveRecord::CounterCacheapi here.The
countmethod incomment.acknowledgers.countis overloaded in ActiveRecord to first check if a counter cache column exists, and if it does, it returns that directly from the model (in this case theCommentmodel) without having to touch the database again.Finally, there was very recently a great Railscast about a gem call Bullet that can help you identify these query issues and guide you toward a solution. It covers both counter caches and N+1 queries.
As @ismaelga pointed out in a comment to this answer, it’s a generally better practice to call
.sizeinstead of.counton a relation. Check out the source forsize:If the relation is already loaded it will just call
lengthon it, otherwise it will callcount. It’s an extra check to try and prevent the database from unnecessarily being queried.