Лучший способ в MySQL для Rails получить AVERAGE в день в пределах определенного диапазона дат
-
05-07-2019 - |
Вопрос
Я пытаюсь создать график в Rails, например, среднюю сумму продаж в день за каждый день в заданном диапазоне дат
Скажем, у меня есть модель products_sold, в которой есть " sales_price " атрибут float. Но если в конкретный день нет продаж (например, нет в модели / db), я хочу просто вернуть 0.
Как лучше всего это сделать в MySQL / Rails? Я знаю, что могу сделать что-то вроде этого:
( Этот запрос SQL может быть совершенно неверным способом получить то, что я тоже хочу )
SELECT avg(sales_price) AS avg, DATE_FORMAT(created_at, '%m-%d-%Y') AS date
FROM products_sold WHERE merchant_id = 1 GROUP BY date;
И получите такие результаты:
| avg | date | 23 01-03-2009 50 01-05-2009 34 01-07-2009 ... ...
Я бы хотел получить следующее:
| avg | date | 23 01-03-2009 0 01-04-2009 50 01-05-2009 0 01-06-2009 34 01-07-2009 0 01-08-2009 ... ...
Могу ли я сделать это с помощью SQL или мне придется постобработать результаты, чтобы найти даты в диапазоне дат, которых нет в наборе результатов SQL? Возможно, мне нужны какие-то подвыборы или операторы IF?
Спасибо всем за помощь.
Решение
Есть ли причина (кроме уже упомянутой даты), почему бы вам не использовать возможности встроенной функции группы в ActiveRecord? Вы, похоже, обеспокоены «постобработкой», о которой я не думаю, что на самом деле стоит о чем-то беспокоиться.
Вы находитесь в Rails, поэтому вам, вероятно, следует сначала поискать решение Rails [1]. Моей первой мыслью было бы сделать что-то вроде
Product.average(:sales_price, :group => "DATE(created_at)", :conditions => ["merchant_id=?", 1])
который ActiveRecord превратил в SQL, который вы описали. Предполагая, что между Merchant и Product существует объявленная has_many
связь, вам, вероятно, будет лучше использовать это, так что-то вроде:
ave_prices = Merchant.find(1).products.average(:sales_price, :group => "DATE(created_at)")
(я надеюсь, что ваше описание модели как "products_sold" является какой-то ошибкой транскрипции, кстати, если нет, то вы несколько не согласны с именами классов!) р>
После всего этого вы вернулись к тому, с чего начали, но попали туда более традиционным способом Rails (а Rails действительно ценит соглашения!). Теперь нам нужно заполнить пробелы.
Я предполагаю, что вы знаете свой диапазон дат, скажем, он определен как все даты от from_date
до to_date
.
date_aves = (from_date..to_date).map{|dt| [dt, 0]}
Это строит полный список дат в виде массива. Нам не нужны даты, когда мы получили среднее значение:
ave_price_dates = ave_prices.collect{|ave_price| ave_price[0]} # build an array of dates
date_aves.delete_if { |dt| ave_price.dates.index(dt[0]) } # remove zero entries for dates retrieved from DB
date_aves.concat(ave_prices) # add the query results
date_aves.sort_by{|ave| ave[0] } # sort by date
Эта партия выглядит немного загроможденной для меня: я думаю, что она может быть более чистой и чистой. Я бы исследовал создание Hash или Struct, а не оставался в массивах.
<Ч> [1] Я не говорю, не используйте SQL - случаются ситуации, когда ActiveRecord не может сгенерировать наиболее эффективный запрос, и вы прибегаете к find_by_sql
. Это нормально, это должно быть так, но я думаю, что вы должны пытаться использовать это только в качестве крайней меры.
Другие советы
Для любого такого запроса вам нужно будет найти механизм для создания таблицы с одной строкой для каждой даты, о которой вы хотите сообщить. Затем вы выполните внешнее соединение этой таблицы с таблицей данных, которую вы анализируете. Возможно, вам также придется поиграть с NVL или COALESCE, чтобы преобразовать нули в нули.
Сложной частью является разработка (временной) таблицы, которая содержит список дат для диапазона, который необходимо проанализировать. Это зависит от СУБД.
Тем не менее, ваша идея сопоставления значений даты / времени с одной датой уместна. Вам нужно было бы применить аналогичный прием - сопоставить все даты в формате даты ISO 8601, например 2009-W01 для недели 01, - если вы хотите проанализировать еженедельные продажи.
Кроме того, вам лучше сопоставить формат DATE с нотацией 2009-01-08, потому что тогда вы можете сортировать в порядке дат, используя сортировку в виде простых символов.
Чтобы немного высохнуть:
ave_prices = Merchant.find(1).products.average(:sales_price, :group => "DATE(created_at)")
date_aves = (from_date..to_date).map{|dt| [dt, ave_prices[dt.strftime "%Y-%m-%d"] || 0]}
Есть ли в MySQL функции, возвращающие множество? То есть функции, которые возвращают разные значения в каждой строке запроса? В качестве примера из PostgreSQL вы можете сделать:
select 'foo', generate_series(3, 5);
Это создаст результирующий набор, состоящий из 2 столбцов и 3 строк, где левый столбец содержит «foo» в каждой строке, а правый столбец содержит 3, 4 и 5.
Итак, предположим, что у вас есть эквивалент generate_series ()
в MySQL и подзапросов: вам нужен LEFT OUTER JOIN
из этой функции для запроса, который вы уже есть. Это обеспечит отображение каждой даты в выходных данных:
SELECT
avg(sales_price) as avg,
DATE_FORMAT(the_date, '%m-%d-%Y') as date
FROM (select cast('2008-JAN-01' as date) + generate_series(0, 364) as the_date) date_range
LEFT OUTER JOIN products_sold on (the_date = created_at)
WHERE merchant_id = 1
GROUP BY date;
Возможно, вам придется немного поиграться, чтобы получить правильный синтаксис для MySQL.