Оптимизация Activerecord - лучший способ сделать запрос одновременно? - PullRequest
3 голосов
/ 13 июля 2011

Я пытаюсь добиться за счет сокращения количества запросов с использованием ActiveRecord 3.0.9. Я генерировал информацию о «фиктивных» клиентах в 200 000 и 500 000 заказов.

Вот модели:

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) Я пытался использовать include (: orders), но в него было включено более 200 000 order_ids. это проблема.

2) они уже проиндексированы.

Ответы [ 3 ]

3 голосов
/ 13 июля 2011

Если вы используете только COUNT и SUM(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]
0 голосов
/ 13 июля 2011

Вы можете объединить таблицы с помощью 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, заполненных только тем, что выуказано в методе выбора.Вы должны быть осторожны, потому что теперь у вас есть в наличии только для чтения объекты, которые, возможно, находятся в недопустимом состоянии.

Как и во всех методах Arel, вы получаете из этих методов экземпляр ActiveRecord :: Relation.Только когда вы начинаете обращаться к этим данным, они выходят и выполняют SQL.

Я немного нервничаю из-за того, что мой синтаксис неверен, но я уверен, что это можно сделать, не полагаясь на выполнение необработанного SQL.

0 голосов
/ 13 июля 2011

Вы можете попробовать что-то вроде этого (да, это выглядит некрасиво, но вы хотите производительность):

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 и amount кеш в Orders, но вам придется его поддерживать (count может быть встречным кешем, но я сомневаюсь, что есть готовый рецепт для amount).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...