Search code examples
ruby-on-railsdatabasepostgresqlperformanceactiveadmin

Rails ActiveAdmin Eliminating n+1 queries on custom attributes belong to another model


I'm using activeadmin for showing branch model records, the branch model has many branchphones, and I want to show the first branchphone created for this branch as an extra custom attribute on activeadmin branch model screen, so I wrote it like that

    show do
    attributes_table do
      row :id
      row :manager_name
      row :manager_email
      row :phone_number do |branch|
        branch&.branch_phones&.order(created_at: :asc)&.first&.phone_number
      end
    end
  end

  index do
    column :id
    column :manager_name
    column :manager_email
    column :phone_number do |branch|
      branch&.branch_phones&.order(created_at: :asc)&.first&.phone_number
    end
    actions
  end

the problem with that code is that it causes me n+1 queries, every time the code get a branch, it goes and create an extra query inside it to get the branch's branch phones, which results on something like that when calling the branch model screen on activeadmin

Processing by Admin::BranchesController#index as HTML
  Parameters: {"subdomain"=>""}
  AdminUser Load (0.8ms)  SELECT  "admin_users".* FROM "admin_users" WHERE "admin_users"."id" = $1 ORDER BY "admin_users"."id" ASC LIMIT $2  [["id", 1], ["LIMIT", 1]]
  Rendering /usr/local/bundle/gems/activeadmin-2.7.0views/active_admin/resource/index.html.arb
   (0.6ms)  SELECT COUNT(*) FROM (SELECT  1 AS one FROM "branches" LIMIT $1 OFFSET $2) subquery_for_count  [["LIMIT", 30], ["OFFSET", 0]]
  CACHE  (0.1ms)  SELECT COUNT(*) FROM (SELECT  1 AS one FROM "branches" LIMIT $1 OFFSET $2) subquery_for_count  [["LIMIT", 30], ["OFFSET", 0]]
   (0.4ms)  SELECT COUNT(*) FROM "branches"
  CACHE  (0.0ms)  SELECT COUNT(*) FROM (SELECT  1 AS one FROM "branches" LIMIT $1 OFFSET $2) subquery_for_count  [["LIMIT", 30], ["OFFSET", 0]]
  Branch Load (0.5ms)  SELECT  "branches".* FROM "branches" ORDER BY "branches"."id" desc LIMIT $1 OFFSET $2  [["LIMIT", 30], ["OFFSET", 0]]
  Store Load (0.6ms)  SELECT "stores".* FROM "stores" WHERE "stores"."id" IN ($1, $2, $3)  [["id", 21], ["id", 1], ["id", 2]]
  BranchPhone Load (0.4ms)  SELECT "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)  [["branch_id", 25], ["branch_id", 24], ["branch_id", 22], ["branch_id", 20], ["branch_id", 19], ["branch_id", 14], ["branch_id", 11], ["branch_id", 4], ["branch_id", 3], ["branch_id", 2], ["branch_id", 1]]
  BranchPhone Load (0.7ms)  SELECT  "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" = $1 ORDER BY "branch_phones"."created_at" ASC LIMIT $2  [["branch_id", 25], ["LIMIT", 1]]
  BranchPhone Load (0.5ms)  SELECT  "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" = $1 ORDER BY "branch_phones"."created_at" ASC LIMIT $2  [["branch_id", 24], ["LIMIT", 1]]
  BranchPhone Load (0.3ms)  SELECT  "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" = $1 ORDER BY "branch_phones"."created_at" ASC LIMIT $2  [["branch_id", 22], ["LIMIT", 1]]
  BranchPhone Load (0.6ms)  SELECT  "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" = $1 ORDER BY "branch_phones"."created_at" ASC LIMIT $2  [["branch_id", 20], ["LIMIT", 1]]
  BranchPhone Load (0.3ms)  SELECT  "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" = $1 ORDER BY "branch_phones"."created_at" ASC LIMIT $2  [["branch_id", 19], ["LIMIT", 1]]
  BranchPhone Load (0.3ms)  SELECT  "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" = $1 ORDER BY "branch_phones"."created_at" ASC LIMIT $2  [["branch_id", 14], ["LIMIT", 1]]
  BranchPhone Load (0.2ms)  SELECT  "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" = $1 ORDER BY "branch_phones"."created_at" ASC LIMIT $2  [["branch_id", 11], ["LIMIT", 1]]
  BranchPhone Load (0.2ms)  SELECT  "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" = $1 ORDER BY "branch_phones"."created_at" ASC LIMIT $2  [["branch_id", 4], ["LIMIT", 1]]
  BranchPhone Load (0.3ms)  SELECT  "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" = $1 ORDER BY "branch_phones"."created_at" ASC LIMIT $2  [["branch_id", 3], ["LIMIT", 1]]
  BranchPhone Load (0.2ms)  SELECT  "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" = $1 ORDER BY "branch_phones"."created_at" ASC LIMIT $2  [["branch_id", 2], ["LIMIT", 1]]
  BranchPhone Load (0.2ms)  SELECT  "branch_phones".* FROM "branch_phones" WHERE "branch_phones"."branch_id" = $1 ORDER BY "branch_phones"."created_at" ASC LIMIT $2  [["branch_id", 1], ["LIMIT", 1]]
  Rendered /usr/local/bundle/gems/activeadmin-2.7.0views/active_admin/resource/index.html.arb (219.4ms)
Completed 200 OK in 258ms (Views: 208.7ms | ActiveRecord: 23.2ms)

so, any solutions to eliminate the n+1 query for the branch phone ? knowing that I tried to eliminate it using controller actions like this enter image description here

but still the branch phones N+1 query not solved

enter image description here


Solution

  • You are getting (N+1) query because you are ordering branch_phones. By default Rails returns records in assenting order. You don't have to order again.

    I believe you have some association like bellow:

    # app/models/branch.rb
    
    class Branch < ApplicationRecord
      has_many :branch_phones
    end
    
    # app/models/branch_phone.rb
    
    class BranchPhone < ApplicationRecord
      belongs_to :branch
    end
    

    Please add this lines in your app/admin/brach.rb file.

    # app/admin/brach.rb
    
    controller do
      def scoped_collection
        super.includes(:branch_phones)
      end
    end
    
    index do
      column :id
      column :manager_name
      column :manager_email
      column :phone_number do |branch|
        branch.branch_phones.first&.phone_number
      end
    
      actions
    end