Примечание: для TLDR, пропустите до конца.
Ваша проблема - очень интересный случай из учебника, так как включает в себя несколько аспектов Postgres.
Я часто нахожу очень полезным разложитьпроблема в нескольких подзадачах перед их объединением для получения окончательного набора результатов.
В вашем случае я вижу две подзадачи: поиск самого популярного продукта за каждый месяц и поиск наименее популярного продукта за каждый месяц.
Давайте начнем с самых популярных продуктов:
WITH months AS (
SELECT generate_series AS month
FROM generate_series(1, 12)
)
SELECT DISTINCT ON (month)
month,
prod,
SUM(quant)
FROM months
LEFT JOIN sales USING (month)
GROUP BY month, prod
ORDER BY month, sum DESC;
Пояснения:
WITH
это общее табличное выражение , которое действуеткак временная таблица (на время запроса) и помогает уточнить запрос. Если вас это смущает, вы также можете выбрать подзапрос. generate_series(1, 12)
- это функция Postgres , которая генерирует серию целых чисел, в данном случае от 1 до 12. LEFT JOIN
позволяет нам связать каждую продажу с соответствующим месяцем. Если за данный месяц продажа не найдена, возвращается строка с месяцем и объединенные столбцы со значениями NULL
. Более подробную информацию о соединениях можно найти здесь . В вашем случае важно использовать LEFT JOIN
, так как использование INNER JOIN
исключает продукты, которые никогда не продавались (что в этом случае должно быть наименее популярным). GROUP BY
используется для суммированиясверх количеств. - на данном этапе, вы должны - потенциально - иметь несколько продуктов за любой данный месяц. Мы хотим сохранить только те из них, которые имеют наибольшее количество за каждый месяц.
DISTINCT ON
особенно полезно для этой цели. Учитывая столбец, он позволяет нам сохранить первую итерацию каждого значения. Поэтому важно сначала ORDER
продавать по сумме, так как будет выбран только первый. Сначала нужно большее число, поэтому следует использовать DESC
(в порядке убывания).
Теперь мы можем повторить процесс для наименее популярных продуктов:
WITH months AS (
SELECT generate_series AS month
FROM generate_series(1, 12)
)
SELECT DISTINCT ON (month)
month,
prod,
SUM(quant)
FROM months
LEFT JOIN sales USING (month)
GROUP BY month, prod
ORDER BY month, sum;
Заключение (и TLDR):
Теперь нам нужно объединить два запроса в один итоговый запрос.
WITH months AS (
SELECT generate_series AS month
FROM generate_series(1, 12)
), agg_sales AS (
SELECT
month,
prod,
SUM(quant)
FROM months
LEFT JOIN sales USING (month)
GROUP BY month, prod
), most_popular AS (
SELECT DISTINCT ON (month)
month,
prod,
sum
FROM agg_sales
ORDER BY month, sum DESC
), least_popular AS (
SELECT DISTINCT ON (month)
month,
prod,
sum
FROM agg_sales
ORDER BY month, sum
)
SELECT
most_popular.month,
most_popular.prod AS most_popular_prod,
most_popular.sum AS most_pop_total_q,
least_popular.prod AS least_popular_prod,
least_popular.sum AS least_pop_total_q
FROM most_popular
JOIN least_popular USING (month);
Обратите внимание, что я использовал промежуточный agg_sales
CTEпопытаться сделать запрос немного понятнее и избегать повторения одной и той же операции дважды, хотя это не должно быть проблемой для оптимизатора Postgres.
Надеюсь, вы найдете мой ответ удовлетворительным. Не стесняйтесь комментировать иное!
РЕДАКТИРОВАТЬ: хотя это решение должно работать как есть, я бы посоветовал хранить ваши даты в виде одного столбца типа TIMESTAMPTZ
. Часто гораздо проще манипулировать датами с использованием этого типа, и это всегда хорошая практика, если вам необходимо проанализировать и провести аудит вашей базы данных далее.
Вы можете получить месяц любой даты, просто используя EXTRACT(MONTH FROM date)
.