【问题标题】:Rails: Optimize querying maximum values from associated tableRails:优化从关联表中查询最大值
【发布时间】:2015-12-01 10:00:17
【问题描述】:

我需要显示合作伙伴列表以及Klass 表中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 运行。

SELECT MAX("klasses"."reservation_limit") FROM "klasses" WHERE "klasses"."partner_id" = $1  [["partner_id", 1]]

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


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

【问题讨论】:

    标签: sql ruby-on-rails ruby postgresql rails-postgresql


    【解决方案1】:

    您可以使用两种形式的 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 要求您使用“按伙伴分组。*”,但就所需的排序和溢出到磁盘的可能性而言,这可能会很昂贵。

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

    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 中可能会在内部有效地转换为第一种形式,但由于在合作伙伴表中每行执行一次子查询(仍将是比实际提交每行查询的原始 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 版本。

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

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

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

    partner.max_klasses_limit
    

    【讨论】:

      【解决方案2】:

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

      改变

      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 不会。

      【讨论】:

      • 我试过但显示为Up to #&lt;Klass:0x007fe9b291bec0&gt; visits per month
      • 查看我更新的答案,最后你需要一个.limit
      • 现在我得到:undefined method 'limit' for nil:NilClass
      • 这是非常低效的,因为它依赖于 Ruby 端的排序,而您在 Ruby 中所做的所有事情都应该在数据库级别执行,因为数据库通常更高效(不考虑查询不会有为类返回所有数据,而 Ruby 不会分配大量对象,有效地浪费大量内存)。
      • @i.am.noob 你明白了,因为你的一些合作伙伴没有课程。检查我更新的答案。
      【解决方案3】:

      如果您开始用纯 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
      

      【讨论】:

      • 我在这里看到几个问题:当您使用“join(:klasses}”时,这不是创建笛卡尔积,它只是将产品与 klasses 内部连接。您需要的原因group('partners.id') 然后将集合减少到每个合作伙伴一行,但您可以通过首先不加入 klasses 来做到这一点。如果您使用子查询来获取 max(klasses.limit ) 而不是加入是多余的,并且仅用于确保不会返回任何合作伙伴,除非他们至少有一个 klass 记录。
      • @DavidAldridge 你是对的,我最初只基于连接使用不同的方法,但后来我使用了子查询。我将保留加入,因为用户在其他 cmets 中遇到的问题之一是存在没有课程的合作伙伴。感谢您的反馈。
      【解决方案4】:

      这将返回最大值。对 parthner_ids 数组的一次选择限制:

      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 %>
      

      【讨论】:

      • 当他需要计算最大值的所有信息都已经存在时,他没有必要编写一个可怕的查询。
      • @Mihai-AndreiDinculescu 他在一次选择中要求 MAX,而不是单独为每个合作伙伴。
      • 我如何获得parthner_ids
      • parthner_ids = @partners.map{|p| p.id}
      • @i.am.noob 我还添加了有关如何将其集成到您的视图中的代码
      猜你喜欢
      • 1970-01-01
      • 2015-10-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-06
      相关资源
      最近更新 更多