Counting data is something we normally do. We count cart items in e-commerce websites, answers to questions in question and answer websites. Bullet is a gem that helps you increase your application’s performance by reducing the number of queries it makes.
Here is an example of a request that has a N+1 issue
Question Load (123.0ms) SELECT "questions".* FROM "questions" [active_model_serializers] (0.9ms) SELECT COUNT(*) FROM "answers" WHERE "answers"."question_id" = $1 [["question_id", 9]] [active_model_serializers] app/serializers/question_serializer.rb:7 [active_model_serializers] (3.6ms) SELECT COUNT(*) FROM "answers" WHERE "answers"."question_id" = $1 [["question_id", 2]] [active_model_serializers] app/serializers/question_serializer.rb:7
Bullet will show this in the log as:
[active_model_serializers] app/serializers/question_serializer.rb:7 user: theuser GET /api/v1/questions Need Counter Cache Question => [:answers]
Need Counter Cache
Here is how to do caching counters with ActiveRecord’s counter caches.
To create a
counter cache, we should add a field to the parent table. In this case, it is the
class AddAnswersCountToQuestions < ActiveRecord::Migration[5.2] def change add_column :questions, :answers_count, :integer end end
When we request from our application, we can see that only one query is applied
Started GET "/api/v1/questions" for ::1 at 2019-08-31 09:13:55 +0800 Processing by Api::V1::QuestionsController#index as JSON Question Load (8.5ms) SELECT "questions".* FROM "questions" app/controllers/api/v1/questions_controller.rb:8 [active_model_serializers] Rendered ActiveModel::Serializer::CollectionSerializer with ActiveModelSerializers::Adapter::Attributes (6.34ms) Completed 200 OK in 84ms (Views: 61.6ms | ActiveRecord: 18.8ms)