具有关联统计信息 N+1 的 Rails 6 索引资源?

问题描述 投票:0回答:2

在controller中,调用Review.all时,我还需要获取关联的统计信息:ranks。
它看起来像这样:

class Review
  has_many :ranks, as: :rankable, dependent: :destroy
  
  def rank
    ranks.average(:score).to_f || 0
  end
  
  def ranks_count
    ranks.count
  end
end

class Rank < ApplicationRecord
  belongs_to :rankable, polymorphic: true
end

class ReviewsController < ApplicationController
  def index
    @reviews = Review.all.page(params[:page])
  end
end


# index.htlm.slim:
  - @reviews.each do |review|
    li= link_to review
      span
        => review.title
        => review.rank
        => review.ranks_count

结果,对于@reviews 集合中的每个对象,我得到两个 SQL 子查询:SELECT AVG 和 SELECT COUNT。
我该如何解决这个问题?

  1. 我看到创建了一个额外的多态 Stat 模型,我将在其中存储 AVG 和 COUNT。
  2. 有 Review.includes(:ranks) 但我不明白如何在这里正确使用它。
  3. 我还可以根据@reviews 进行分组查询
    像这样的东西:
@ranks = Rank.select(:score, :rankable_id).where(rankable: @reviews, rankable_id: @reviews.ids).group_by(&:rankable_id)

然后在视图中进一步使用它。

解决这个问题的最好方法是什么,是否可以通过包含很好地解决它?

sql ruby-on-rails activerecord orm model-associations
2个回答
0
投票

避免额外查询的一种方法是不使用 average 和 count 实例方法——因为这将需要再次加载所有内容,但让您的查询立即返回平均值和计数;

Review
  .joins(:ranks)
  .select('avg(score)::float, count(ranks.id)')
  .group(:id)

像这样的东西;

-- select * from reviews;

 id | title |         created_at         |         updated_at
----+-------+----------------------------+----------------------------
  1 | 1st   | 2023-03-28 06:48:12.375043 | 2023-03-28 06:48:12.375043
  2 | 2nd   | 2023-03-28 06:48:14.333537 | 2023-03-28 06:48:14.333537
(2 rows)

-- select * from ranks;
 id | review_id | score |         created_at         |         updated_at
----+-----------+-------+----------------------------+----------------------------
  1 |         1 |     1 | 2023-03-28 06:48:26.513838 | 2023-03-28 06:48:26.513838
  2 |         1 |     2 | 2023-03-28 06:48:28.565594 | 2023-03-28 06:48:28.565594
  3 |         1 |     2 | 2023-03-28 06:50:25.135863 | 2023-03-28 06:50:25.135863
  4 |         2 |     5 | 2023-03-28 06:52:46.001207 | 2023-03-28 06:52:46.001207
(4 rows)

你会得到这个;

Review.joins(:ranks).select('avg(score)::float, count(ranks.id)').group(:id).as_json

SELECT avg(score)::float, count(ranks.id) FROM "reviews" INNER JOIN "ranks" ON "ranks"."review_id" = "reviews"."id" GROUP BY "reviews"."id"

# => [{"avg"=>5.0, "count"=>1, "id"=>nil}, {"avg"=>1.6666666666666667, "count"=>3, "id"=>nil}]

as_json
不是必须的,只是一种表现方式)


0
投票

您可以进行左连接并从连接的表中选择聚合:

class Review < ApplicationRecord
   has_many :ranks, as: :rankable, dependent: :destroy

  def self.with_stats
    ranks = Rank.arel_table
    left_joins(:ranks)
      .group(:id)
      .select(
        arel_table[Arel.star], # everything from the reviews table
        ranks[:id].count.as("rank_count") 
        ranks[:score].avg.as("rank")
      )
  end
end

如果您使用的是 Postgres,则可以使用

ranks[Arel.star].count
,它比
COUNT("ranks"."id")
稍快。

如果您经常使用 Rank 计数,您也可以使用 counter cache 作为读取优化。

© www.soinside.com 2019 - 2024. All rights reserved.