【问题标题】:Activerecord opitimization - best way to query all at once?Activerecord 优化 - 一次查询的最佳方式?
【发布时间】:2011-07-14 03:25:16
【问题描述】:

我试图通过使用 ActiveRecord 3.0.9 减少查询数量来实现。我产生了大约 20 万个“虚拟”客户和 50 万个订单。

以下是模特:

class Customer < ActiveRecord::Base
  has_many :orders
end

class Orders < ActiveRecord::Base
  belongs_to :customer
  has_many :products
end

class Product < ActiveRecord::Base
  belongs_to :order
end

当您在控制器中使用此代码时:

@customers = Customer.where(:active => true).paginate(page => params[:page], :per_page => 100)
# SELECT * FROM customers ...

并在视图中使用它(我删除了 HAML 代码以便于阅读):

@order = @customers.each do |customer|
  customer.orders.each do |order|      # SELECT * FROM orders ...
    %td= order.products.count          # SELECT COUNT(*) FROM products ...
    %td= order.products.sum(:amount)   # SELECT SUM(*) FROM products ...
  end
end

但是,页面呈现为每页 100 行的表格。问题是加载速度有点慢,因为它会为每个客户的订单触发大约 3-5 个查询。加载页面大约需要 300 个查询。

还有其他方法可以减少查询次数并更快地加载页面吗?

注意事项:

1) 我曾尝试使用includes(:orders),但它包含超过200,000 个order_id。这就是问题。

2) 它们已被编入索引。

【问题讨论】:

    标签: ruby-on-rails ruby ruby-on-rails-3 activerecord


    【解决方案1】:

    如果您只使用COUNTSUM(amount),那么您真正需要的是仅检索该信息而不是订单本身。使用 SQL 很容易做到这一点:

    SELECT customer_id, order_id, COUNT(id) AS order_count, SUM(amount) AS order_total FROM orders LEFT JOIN products ON orders.id=products.order_id GROUP BY orders.customer_id, products.order_id
    

    您可以通过将 SQL 结果重新映射到适合您要求的结构将其包装在一个返回漂亮、有序散列的方法中:

    class Order < ActiveRecord::Base
      def self.totals
        query = "..." # Query from above
    
        result = { }
    
        self.connection.select_rows(query).each do |row|
          # Build out an array for each unique customer_id in the results
          customer_set = result[row[0].to_i] ||= [ ]
    
          # Add a hash representing each order to this customer order set
          customer_set << { :order_id => row[1].to_i, :count => row[2].to_i, :total => row[3].to_i } ]
        end
    
        result
      end
    end
    

    这意味着您可以一次获取所有订单计数和总数。如果您在customer_id 上有一个索引,在这种情况下这是必不可少的,那么即使对于大量行,查询通常也会非常快。

    您可以将该方法的结果保存到一个变量中,例如@order_totals,并在渲染表格时引用它:

    - @order = @customers.each do |customer|
      - @order_totals[customer.id].each do |order|
        %td= order[:count]
        %td= order[:total]
    

    【讨论】:

      【解决方案2】:

      你可以尝试这样的事情(是的,它看起来很丑,但你想要性能):

      orders = Order.find_by_sql([<<-EOD, customer.id])
      
      SELECT os.id, os.name, COUNT(ps.amount) AS count, SUM(ps.amount) AS amount 
      FROM orders os 
        JOIN products ps ON ps.order_id = os.id 
      WHERE os.customer_id = ? GROUP BY os.id, os.name
      
      EOD
      
      %td= orders.name
      %td= orders.count
      %td= orders.amount
      

      补充:Orders中创建countamount缓存可能会更好,但你必须维护它(count可以是反缓存,但我怀疑amount 有现成的配方。

      【讨论】:

        【解决方案3】:

        您可以使用 Arel 将表连接起来(我希望尽可能避免编写原始 sql)。我相信对于您的示例,您会执行以下操作:

        Customer.joins(:orders -> products).select("id, name, count(products.id) as count, sum(product.amount) as total_amount")
        

        第一种方法——

        Customer.joins(:orders -> products)
        

        --在一个语句中拉入嵌套关联。然后是第二部分——

        .select("id, name, count(products.id) as count, sum(product.amount) as total_amount")
        

        --指定您想要返回的确切列。

        将这些链接起来,我相信您会得到一个 Customer 实例列表,其中仅填充了您在 select 方法中指定的内容。但是您必须小心,因为您现在手头有可能处于无效状态的只读对象。

        与所有 Arel 方法一样,您从这些方法中获得的是一个 ActiveRecord::Relation 实例。只有当您开始访问该数据时,它才会发出并执行 SQL。

        我对我的语法不正确有一些基本的紧张,但我相信这可以在不依赖执行原始 SQL 的情况下完成。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-09-02
          • 2014-07-19
          • 1970-01-01
          • 1970-01-01
          • 2016-08-06
          • 1970-01-01
          相关资源
          最近更新 更多