Лучший способ в MySQL для Rails получить AVERAGE в день в пределах определенного диапазона дат

StackOverflow https://stackoverflow.com/questions/423212

Вопрос

Я пытаюсь создать график в 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.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top