Activerecord优化 - 一次查询所有内容的最佳方式?

时间:2011-07-13 17:05:39

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

我试图通过减少使用ActiveRecord 3.0.9的查询数量来实现。我生成了关于'假'的200K客户和500K订单。

这是模特:

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)他们已被编入索引。

3 个答案:

答案 0 :(得分:3)

如果您只使用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]

答案 1 :(得分:0)

你可以尝试这样的事情(是的,它看起来很难看,但你想要表现):

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

已添加:可能最好在count中创建amountOrders缓存,但您必须对其进行维护(count可以是反缓存,但我怀疑amount)有一个现成的配方。

答案 2 :(得分:0)

您可以使用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")

- 准确指定您想要的列。

将这些链接起来,我相信您将获得仅使用您在select方法中指定的内容填充的Customer实例列表。你必须要小心,因为你现在手边只能读取可能处于无效状态的对象。

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

我有一些基本的紧张感,我的语法不正确,但我相信这可以完全依赖于执行原始SQL。