Собесов

Сценарий: динамический PIVOT в SQL

SQLPivotСложнаяMiddle

Условие

Таблица 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-таблица.

Подводные камни

  1. Полностью динамические колонки нельзя в одном SQL — требуется procedure / приложение для сборки списка.
  2. crosstab требует extension tablefunc в PostgreSQL.
  3. NULL → нужно COALESCE(value, 0) после pivot.
  4. Если в данных встречается продукт, у которого нет sales в каком-то месяце, ячейка будет NULL — это правильно, но иногда хотят 0.
  5. Огромное число «колонок-месяцев» → query план тормозит. Лучше long + BI tool.

Эталонный ответ

Статический: SUM(CASE WHEN month='X' THEN revenue END). Динамический: crosstab (PG) с явными колонками, или jsonb_object_agg если колонок много. Полностью dynamic — генерация SQL в procedure или BI pivot.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти