SQL-запрос, который выполняет две ГРУППОВЫЕ операции?

#sql #ruby-on-rails #postgresql

#sql #ruby-on-rails #postgresql

Вопрос:

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

  • Таблица articles (такие поля, как as name, manufacturer_id и т.д.).
  • Таблица sales .
    • FK к статьям, называемым article_id
    • Целое число, называемое amount
    • date Поле
    • Вызываемое поле type . Мы можем предположить, что это строка, и она может иметь 3 известных значения — 'morning' , 'evening' и 'night'

Я хочу сгенерировать сводный отчет о продажах с указанием даты начала и даты окончания:

   -------------- --------------- -------------- ------------- 
 | article_name | morning_sales | evening_sales| night_sales |
  -------------- --------------- -------------- ------------- 
 | article 1    |             0 |           12 |           2 |
 | article 2    |            80 |            3 |           0 |
...            ...             ...            ...           ...
 | article n    |            37 |           12 |           1 |
  -------------- --------------- -------------- ------------- 
  

Я хотел бы сделать это как можно эффективнее. До сих пор мне удавалось сгенерировать запрос, который даст мне один тип продажи (утренний, вечерний или ночной), но я не могу сделать это для нескольких типов одновременно. Возможно ли это вообще?

Это то, что у меня есть на данный момент; это даст мне название статьи и утренние продажи всех статей за данный период — другими словами, первые две колонки отчета:

 SELECT articles.name as article_name,
       SUM(sales.amount) as morning_sales,
FROM "sales"
INNER JOIN articles ON articles.id = sales.articles_id
WHERE ( sales.date >= '2011-05-09'
    AND sales.end_date <= '2011-05-16'
    AND sales.type = 'morning'
)
GROUP BY sales.article_id
  

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

  • Если мне нужно выполнить 1 запрос для каждого типа продажи, как мне «смешать и сопоставить» разные списки статей, которые я получу?
  • Есть ли лучший способ сделать это (возможно, с помощью каких-либо подзапросов)?

Аналогичным образом, я могу создать запрос, который выдает мне все утренние, вечерние и ночные продажи, сгруппированные по типу. Я предполагаю, что моя проблема в том, что мне нужно выполнить два ГРУППОВЫХ запроса, чтобы получить этот отчет. Я не знаю, как это сделать, если это вообще возможно.

Я использую PostgreSQL в качестве своей базы данных, но я хотел бы оставаться как можно более стандартным. Если это поможет, приложение, использующее это, является приложением Rails.

Ответ №1:

К счастью, вам не нужно выполнять несколько запросов с вашим форматом базы данных. Это должно сработать для вас:

 SELECT
  articles.name AS article_name
  SUM(IF(sales.type = 'morning', sales.amount, 0.0)) AS morning_sales,
  SUM(IF(sales.type = 'evening', sales.amount, 0.0)) AS evening_sales,
  SUM(IF(sales.type = 'night', sales.amount, 0.0)) AS night_sales
FROM sales
  JOIN articles ON sales.article_id = articles.id
WHERE
  sales.date >= "2011-01-01 00:00:00"
  AND sales.date < "2011-02-01 00:00:00"
GROUP BY sales.article_id
  

И если существуют другие типы, вам пришлось бы добавить туда больше столбцов ИЛИ просто суммировать другие типы, добавив это в предложение SELECT:

 SUM(
  IF(sales.type IS NULL OR sales.type NOT IN ('morning', 'evening', 'night'), 
    sales.amount, 0.0
  )
) AS other_sales
  

Вышеуказанное совместимо с MySQL. Чтобы использовать его в Postgres, я думаю, вам пришлось бы изменить IF выражения на CASE expressions, которые должны выглядеть следующим образом (непроверенные):

 SELECT
  articles.name AS article_name,
  SUM(CASE WHEN sales.type = 'morning' THEN sales.amount ELSE 0.0 END) AS morning_sales,
  SUM(CASE WHEN sales.type = 'evening' THEN sales.amount ELSE 0.0 END) AS evening_sales,
  SUM(CASE WHEN sales.type = 'night' THEN sales.amount ELSE 0.0 END) AS night_sales
FROM sales
  JOIN articles ON sales.article_id = articles.id
WHERE
  sales.date >= "2011-01-01 00:00:00"
  AND sales.date < "2011-02-01 00:00:00"
GROUP BY sales.article_id
  

Ответ №2:

Два варианта.

Вариант 1. Одиночное объединение с вычисляемыми столбцами для объединения

 select article_name  = a.article_name ,
       morning_sales = sum( case when sales.type = 'morning' then sales.amount end ) ,
       evening_sales = sum( case when sales.type = 'evening' then sales.amount end ) ,
       night_sales   = sum( case when sales.type = 'night'   then sales.amount end ) ,
       other_sales   = sum( case when sales.type in ( 'morning','evening','night') then null else sales.amount end ) ,
       total_sales   = sum( sales.amount )
from articles a
join sales    s on s.articles_id = a.articles_id
where sales.date between @dtFrom and @dtThru
group by a.article_name
  

Вариант 2. множественные объединения слева

 select article_name = a.article_name ,
       morning_sales = sum( morning.amount ) ,
       evening_sales = sum( evening.amount ) ,
       night_sales   = sum( night.amount   ) ,
       other_sales   = sum( other.amount   ) ,
       total_sales   = sum( total.amount   )
from      articles a
left join sales    morning on morning.articles_id = a.articles_id
                          and morning.type        = 'morning'
                          and morning.date between @dtFrom and @dtThru
left join sales    evening on evening.articles_id = a.articles_id
                          and evening.type        = 'evening'
                          and evening.date between @dtFrom and @dtThru
left join sales    night   on night.articles_id   = a.articles_id
                          and night.type          = 'evening'
                          and night.date between @dtFrom and @dtThru
left join sales    other   on other.articles_id = a.articles_id
                          and (    other.type is null
                                OR other.type not in ('morning','evening','night')
                              )
                          and other.date between @dtFrom and @dtThru
left join sales    total   on total.articles_id = a.articles_id
                          and total.date between @dtFrom and @dtThru
group by a.article_name
  

Комментарии:

1. Довольно хорошо; Мне нравится идея варианта 2 как альтернативы самосоединениям. Но у варианта 2 есть 3 проблемы. (1) Вы выбрали только одно значение, название статьи! Вам также необходимо выбрать значения из самосоединений. (2) В зависимости от того, как используется запрос, могут потребоваться имена столбцов. Итак, запрос должен начинаться с Select a.article_name, Sum(morning.amount)MorningAmount, Sum(evening.amount)EveningAmount, (и так далее). (3) Обратите внимание, что это sum(Total.amount) всегда будет равно sum(a.Amount) , поэтому у вас будет больше самосоединений, чем вам нужно; просто полностью исключите total.

2. Мои запросы соответствуют примеру OP. Но если вам нужно украсить совокупный набор результатов столбцами, отличными от группирующих столбцов или агрегатных функций, уровень сложности повышается. Некоторые люди просто добавляют их в список group by, но, по моему опыту, это приводит к … интересным результатам… когда данные (как всегда) не такие чистые, как должны быть.

3. Большое спасибо за ваш ответ. Я собираюсь выбрать запрос Джона только потому, что он появился немного раньше. Я бы выбрал оба, если бы мог. 1 в любом случае! С уважением!

4. «Назначение» в SELECT кажется очень странным и не будет работать с PostgreSQL. Что это должно делать? И какой диалект SQL позволяет определять переменные в предложении SELECT?

5. select <column_name> = <expression> ... Синтаксис — классический SQL, восходящий (по крайней мере) к DB2 версии 1 release 2. Он поддерживается многими реализациями SQL, но не всеми (заметный Oracle). Это точно так же, как select <expression> as <column_name> . Где вы видите определения переменных в select инструкции? Конечно, есть ссылки на переменные ( @dtFrom / @dtThru ), но нет определений переменных.