Лучший способ в MySQL или Rails, чтобы получить AVG в день в пределах определенного диапазона дат - PullRequest
5 голосов
/ 08 января 2009

Я пытаюсь построить график в Rails, например, среднюю сумму продаж в день за каждый день в заданном диапазоне дат

Допустим, у меня есть модель products_sold, которая имеет атрибут float "sales_price". Но если в конкретный день нет продаж (например, нет в модели / дБ), я хочу вернуть просто 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?

Спасибо за любую помощь.

Ответы [ 4 ]

7 голосов
/ 08 января 2009

Есть ли причина (кроме уже упомянутой даты), почему бы вам не использовать возможности встроенной функции группы в ActiveRecord? Вы, кажется, обеспокоены «постобработкой», о которой я не думаю, что действительно стоит беспокоиться.

Вы находитесь в Rails, поэтому вам, вероятно, следует сначала поискать решение Rails [1]. Моей первой мыслью было бы сделать что-то вроде

Product.average(:sales_price, :group => "DATE(created_at)", :conditions => ["merchant_id=?", 1])

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

2 голосов
/ 19 июля 2009

Чтобы немного высохнуть:

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]}
2 голосов
/ 08 января 2009

Для любого такого запроса вам нужно будет найти механизм для создания таблицы с одной строкой для каждой даты, о которой вы хотите сообщить. Затем вы выполните внешнее соединение этой таблицы с таблицей данных, которую вы анализируете. Возможно, вам также придется поиграть с NVL или COALESCE, чтобы преобразовать нули в нули.

Сложной частью является разработка (временной) таблицы, содержащей список дат для диапазона, который необходимо проанализировать. Это зависит от СУБД.

Тем не менее, ваша идея сопоставления значений даты / времени с одной датой уместна. Вам нужно применить аналогичный прием - сопоставить все даты в формате даты ISO 8601, например 2009-W01 для недели 01, - если вы хотите проанализировать еженедельные продажи.

Кроме того, вам лучше сопоставить формат DATE с нотацией 2009-01-08, потому что тогда вы можете сортировать по дате, используя сортировку в виде простых символов.

0 голосов
/ 08 января 2009

Есть ли в 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.

...