Отчёт по выручке от...
 
Уведомления
Очистить всё

Отчёт по выручке открывается 9 секунд, группирую в Ruby. Как перенести в SQL?

4 Сообщения
3 Пользователи
2 Реакции
870 Просмотры
Валерий40
Сообщения: 33
Автор темы
Читает чужие PR фулстек-разработчик
Присоединился: 4 года назад
[#25]

В Laravel я бы сразу полез в query builder, а тут увлёкся Ruby-методами. Rails 7.0, MySQL 8. За год около 120 тысяч оплаченных заказов, страница отчёта открывается 9 секунд, в логе сотни запросов к order_items.

def revenue_by_month(year)
  orders = Order.where(status: "paid", created_at: Date.new(year).all_year)
  result = Hash.new(0)
  orders.each do |order|
    month = order.created_at.strftime("%Y-%m")
    result[month] += order.items.sum { |i| i.price * i.quantity }
  end
  result
end

Понимаю, что это N+1 и что считать надо в базе. Как это нормально записать в Active Record, не уходя целиком в сырой SQL?


3 ответа
kostya.sokurov
Сообщения: 73
Держит прод бэкенд-разработчик, senior
Присоединился: 4 года назад

Считать надо от позиций, а не от заказов, тогда всё сводится к одному запросу с group и sum:

OrderItem
  .joins(:order)
  .where(orders: { status: "paid", created_at: Time.zone.local(year).all_year })
  .group(Arel.sql("DATE_FORMAT(orders.created_at, '%Y-%m')"))
  .sum(Arel.sql("order_items.price * order_items.quantity"))
# => {"2023-01"=>..., "2023-02"=>..., ...}, значения BigDecimal

Заметь, я поменял Date.new(year).all_year на Time.zone.local. Диапазон дат на datetime-колонке превращается в BETWEEN до '2023-12-31', то есть до полуночи, и почти все продажи 31 декабря теряются. Потом покажи EXPLAIN, на таком объёме всё решает индекс по (status, created_at) на orders. И проверь часовой пояс в группировке: created_at хранится в UTC, так что оплата первого числа до трёх ночи по нашему времени уедет в предыдущий месяц.


Ответ
vova64
Сообщения: 23
Собрал свой gem фрилансер
Присоединился: 4 года назад

В MySQL 8 по умолчанию включён ONLY_FULL_GROUP_BY, так что лишних колонок в select к этому запросу не добавляй, сразу будет ошибка


Ответ
Валерий40
Сообщения: 33
Автор темы
Читает чужие PR фулстек-разработчик
Присоединился: 4 года назад

Сделал через joins и group, добавил индекс (status, created_at) на orders. Было 9,2 секунды, стало 0,6, EXPLAIN показывает range по новому индексу. С 31 декабря kostya.sokurov попал в точку, я терял почти целый день продаж. Таблицы поясов в MySQL у нас не загружены, так что группирую по DATE_FORMAT(CONVERT_TZ(orders.created_at, '+00:00', '+03:00'), '%Y-%m'). С выгрузкой бухгалтерии за сентябрь сошлось до рубля)


Ответ