Rails:优化从关联 table 中查询最大值

Rails: Optimize querying maximum values from associated table

我需要显示合作伙伴列表和 Klass table 中 reservation_limit 列的最大值。

Partner  has_many    :klasses
Klass    belongs_to  :partner

# Partner controller
def index
  @partners = Partner.includes(:klasses)
end

# view
<% @partners.each do |partner| %>
  Up to <%= partner.klasses.maximum("reservation_limit") %> visits per month
<% end %>

不幸的是,每个 Partner.

下的查询 运行s
SELECT MAX("klasses"."reservation_limit") FROM "klasses" WHERE "klasses"."partner_id" =   [["partner_id", 1]]

如果有 40 个合作伙伴,则查询将 运行 40 次。我该如何优化它?


编辑:看起来 rails 中有一个 limit 方法,所以我将有问题的 limit 更改为 reservation_limit 以防止混淆。

这将 return 最大。 parthner_ids:

数组的限制为 select
parthner_ids = @partners.map{|p| p.id}
data = Klass.select('MAX("limit") as limit', 'partner_id').where(partner_id: parthner_ids).group('partner_id')
@limits = data.to_a.group_by{|d| d.id}

您现在可以将其集成到您的视图中:

<% @partners.each do |partner| %>
  Up to <%= @limits[partner.id].limit %> visits per month
<% end %>

您的初始查询提供了您需要的所有信息。您只需像使用常规对象数组一样使用它。

改变

Up to <%= partner.klasses.maximum("reservation_limit") %> visits per month

Up to <%= partner.klasses.empty? ? 0 : partner.klasses.max_by { |k| k.reservation_limit }.reservation_limit %> visits per month

maximum("reservation_limit") 触发 Active Record 查询 SELECT MAX... 的作用。但是您不需要这个,因为您已经拥有处理数组中最大值所需的所有信息。

备注
在 Active Record 结果上使用 .count 将触发额外的 SELECT COUNT... 查询!
使用 .length 不会。

如果您开始使用纯 SQL 编写查询,然后将其提取到 ActiveRecord 或 Arel 代码中,通常会有所帮助。

ActiveRecord 很强大,但一旦偏离标准的 CRUD 操作,它往往会迫使您编写非常低效的查询。

这是您的查询

Partner
    .select('partners.*, (SELECT MAX(klasses.reservation_limit) FROM klasses WHERE klasses.partner_id = partners.id) AS maximum_limit')
    .joins(:klasses).group('partners.id')

这是一个带有子查询的单一查询。但是,子查询仅优化为 运行 一次,因为它可以提前解析,而不是 运行 N+1 次。

上面的代码获取所有合作伙伴,将它们与 klasses 记录连接起来,由于连接,它可以计算聚合最大值。由于连接有效地创建了记录的笛卡尔积,因此您需要按 partners.id 分组(实际上 MAX 聚合函数在任何情况下都需要)。

此处的关键是 AS maximum_limit,它会将新属性分配给返回计数值的 Partner 个实例。

partners = Partner.select ...
partners.each do |partner|
  puts partner.maximum_limit
end

您可以使用两种形式的 SQL 来有效地检索此信息,我在这里假设您想要一个合作伙伴的结果,即使它没有 klass 记录

第一个是:

   select partners.*,
          max(klasses.limit) as max_klasses_limit
     from partners
left join klasses on klasses.partner_id = partners.id
 group by partner.id

有些 RDBMS 要求您使用 "group by partner.*",但是,就所需的排序和溢出到磁盘的可能性而言,这可能很昂贵。

另一方面,您可以添加一个子句,例如:

having("max(klasses.limit) > ?", 3)

... 根据合作伙伴的最大值 klass.limit

有效地过滤合作伙伴

另一个是:

   select partners.*,
          (Select max(klasses.limit)
             from klasses
            where klasses.partner_id = partners.id) as max_klasses_limit
     from partners

第二个不依赖于 group by,并且在某些 RDBMS 中可能会在内部有效地转换为第一种形式,但由于在合作伙伴中每行执行一次子查询,因此执行效率可能较低 table(这仍然比原始 Rails 实际提交每行查询的方式快得多)。

这些的 Rails ActiveRecord 形式是:

Partner.joins("left join klasses on klasses.partner_id = partners.id").
        select("partners.*, max(klasses.limit) as max_klasses_limit").
        group(:id)

...和...

Partner.select("partners.*, (select max(klasses.limit)
               from klasses
               where klasses.partner_id = partners.id) as max_klasses_limit")

实际上哪一个最有效可能取决于 RDBMS 甚至 RDBMS 版本。

如果伙伴没有类时不需要结果,或者总是保证有一个,那么:

Partner.joins(:klasses).
        select("partners.*, max(klasses.limit) as max_klasses_limit").
        group(:id)

无论哪种方式,您都可以参考

partner.max_klasses_limit