Условие
Таблица sales(month, product, revenue). Нужно вывести pivot: строки — products, столбцы — months. Список месяцев не зафиксирован (динамический).
Решение
Статический pivot (если месяцев известно)
SELECT
product,
SUM(CASE WHEN month = '2024-01' THEN revenue END) AS jan,
SUM(CASE WHEN month = '2024-02' THEN revenue END) AS feb,
SUM(CASE WHEN month = '2024-03' THEN revenue END) AS mar
FROM sales
GROUP BY product;Динамический pivot — PostgreSQL crosstab
CREATE EXTENSION IF NOT EXISTS tablefunc;
SELECT *
FROM crosstab(
$$SELECT product, month, revenue FROM sales ORDER BY 1, 2$$,
$$SELECT DISTINCT month FROM sales ORDER BY 1$$
) AS ct (
product text,
"2024-01" numeric,
"2024-02" numeric,
"2024-03" numeric
);Минус: column names всё равно нужно задать заранее.
Динамика через генерацию SQL
В большинстве БД полностью динамический pivot — это сгенерировать SQL и execute:
DO $$
DECLARE
cols text;
query text;
BEGIN
SELECT string_agg(quote_ident(month), ', ')
INTO cols
FROM (SELECT DISTINCT month FROM sales ORDER BY 1) sub;
query := format(
'SELECT * FROM crosstab(
''SELECT product, month, revenue FROM sales ORDER BY 1, 2'',
''SELECT DISTINCT month FROM sales ORDER BY 1''
) AS ct (product text, %s)',
cols
);
EXECUTE query;
END $$;Это работает в процедуре, но результат не возвращается в обычный SELECT.
Альтернатива: вернуть в JSON
SELECT product,
jsonb_object_agg(month, revenue) AS by_month
FROM sales
GROUP BY product;Получите {"2024-01": 100, "2024-02": 150, ...} для каждого product. Парсить на стороне приложения.
ClickHouse
SELECT
product,
sumIf(revenue, month = '2024-01') AS jan,
sumIf(revenue, month = '2024-02') AS feb
FROM sales
GROUP BY product;Snowflake / BigQuery
Snowflake поддерживает PIVOT:
SELECT * FROM sales
PIVOT(SUM(revenue) FOR month IN ('2024-01', '2024-02', '2024-03'))В BigQuery:
SELECT *
FROM sales
PIVOT(SUM(revenue) FOR month IN ('2024-01', '2024-02', '2024-03'))Best practice
Большинство BI-инструментов (Tableau, Power BI, Superset) делают pivot на своей стороне. В SQL — long format, в BI — wide. Pivot в SQL — для cases где BI недоступен или нужна экспортируемая wide-таблица.
Подводные камни
- Полностью динамические колонки нельзя в одном SQL — требуется procedure / приложение для сборки списка.
crosstabтребует extensiontablefuncв PostgreSQL.- NULL → нужно
COALESCE(value, 0)после pivot. - Если в данных встречается продукт, у которого нет sales в каком-то месяце, ячейка будет NULL — это правильно, но иногда хотят 0.
- Огромное число «колонок-месяцев» → query план тормозит. Лучше long + BI tool.
Эталонный ответ
Статический: SUM(CASE WHEN month='X' THEN revenue END). Динамический: crosstab (PG) с явными колонками, или jsonb_object_agg если колонок много. Полностью dynamic — генерация SQL в procedure или BI pivot.