PIVOT и UNPIVOT в SQL: шпаргалка

Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.

Что такое PIVOT

Поворот таблицы: строки → колонки. Из длинного формата (long) в широкий (wide).

Пример. Есть продажи по месяцам:

month   category    revenue
2026-01 sql         100
2026-01 python      80
2026-02 sql         120
2026-02 python      90

Хотим получить:

month     sql   python
2026-01   100   80
2026-02   120   90

Это и есть PIVOT.

PIVOT в разных СУБД

СУБД Нативный PIVOT Через CASE Функция
PostgreSQL нет да crosstab (extension tablefunc)
MSSQL да (оператор PIVOT) да
MySQL нет да
Oracle да (11g+) да
BigQuery да (PIVOT в стандарте) да
Snowflake да да
ClickHouse нет да

Универсальный способ — через CASE + GROUP BY. Работает везде.

1. PIVOT через CASE (универсально)

SELECT
    month,
    SUM(CASE WHEN category = 'sql'    THEN revenue ELSE 0 END) AS sql_rev,
    SUM(CASE WHEN category = 'python' THEN revenue ELSE 0 END) AS python_rev
FROM sales
GROUP BY month;

2. PIVOT в MSSQL / Azure SQL

SELECT month, [sql], [python]
FROM (
    SELECT month, category, revenue FROM sales
) src
PIVOT (
    SUM(revenue)
    FOR category IN ([sql], [python])
) pvt;

3. PIVOT в BigQuery / Snowflake

SELECT *
FROM sales
PIVOT (
    SUM(revenue) FOR category IN ('sql', 'python')
);

4. PIVOT в Postgres через crosstab

-- нужен extension:
-- CREATE EXTENSION tablefunc;

SELECT *
FROM crosstab(
    'SELECT month, category, revenue FROM sales ORDER BY 1, 2',
    'SELECT DISTINCT category FROM sales ORDER BY 1'
) AS ct(month DATE, sql NUMERIC, python NUMERIC);

5. Pivot с несколькими агрегатами

SELECT
    month,
    SUM(CASE WHEN category = 'sql'    THEN revenue END) AS sql_rev,
    SUM(CASE WHEN category = 'python' THEN revenue END) AS python_rev,
    COUNT(CASE WHEN category = 'sql'    THEN 1 END) AS sql_orders,
    COUNT(CASE WHEN category = 'python' THEN 1 END) AS python_orders
FROM sales
GROUP BY month;

UNPIVOT — обратная операция

Поворот колонок в строки. Из wide в long.

Было:

month     sql   python
2026-01   100   80

Хотим:

month     category   revenue
2026-01   sql        100
2026-01   python     80

6. UNPIVOT через UNION ALL (универсально)

SELECT month, 'sql' AS category, sql_rev AS revenue FROM sales_wide
UNION ALL
SELECT month, 'python' AS category, python_rev FROM sales_wide;

7. UNPIVOT в MSSQL

SELECT month, category, revenue
FROM sales_wide
UNPIVOT (
    revenue FOR category IN (sql_rev, python_rev)
) upvt;

8. UNPIVOT в Postgres через VALUES / LATERAL

SELECT
    s.month,
    v.category,
    v.revenue
FROM sales_wide s
CROSS JOIN LATERAL (
    VALUES
        ('sql',    s.sql_rev),
        ('python', s.python_rev)
) AS v(category, revenue);

9. UNPIVOT в BigQuery / Snowflake

SELECT * FROM sales_wide
UNPIVOT (
    revenue FOR category IN (sql_rev, python_rev)
);

Когда использовать PIVOT

  • Отчёт в «таблично-сводном» формате для бизнеса
  • Упрощение дашборда (отчёт в Tableau / PowerBI легче строить из wide)
  • Подготовка данных для ML (один признак — одна колонка)

Когда использовать UNPIVOT

  • Нормализация данных после импорта из Excel
  • Визуализация в long-формате (ggplot, seaborn, Tableau)
  • Универсальная обработка временных рядов

Частые ошибки

Ошибка 1. Хардкод всех значений

-- плохо: если добавится новая категория, запрос сломается
SUM(CASE WHEN category = 'sql' THEN revenue END)
SUM(CASE WHEN category = 'python' THEN revenue END)

-- лучше: динамический SQL (но он сложнее)

В production для «неизвестного числа колонок» используют dynamic SQL.

Ошибка 2. COUNT с CASE

-- COUNT считает и NULL, если нет ELSE → неверно
COUNT(CASE WHEN cond THEN 1 ELSE 0 END)  -- считает ВСЕ строки
COUNT(CASE WHEN cond THEN 1 END)  -- считает только где условие истинно

Ошибка 3. Забыть GROUP BY при PIVOT через CASE

-- без GROUP BY вернётся одна строка с агрегатами по всей таблице
SELECT SUM(CASE WHEN ... THEN ... END) FROM sales;

-- правильно: группировка по ключу
SELECT month, SUM(CASE ...) FROM sales GROUP BY month;

Ошибка 4. UNPIVOT с колонками разных типов

-- ошибка: string и number в одной колонке
UNPIVOT (value FOR col IN (name_str, age_int))

-- требует CAST к общему типу

Производительность

  • PIVOT через CASE — такой же по скорости, как обычный GROUP BY
  • Нативный PIVOT (MSSQL, Snowflake) — оптимизированнее, но разница небольшая
  • crosstab в Postgres — быстрее CASE на широких таблицах

Связанные темы

FAQ

Как сделать PIVOT в Postgres?

Через CASE + GROUP BY (универсально) или через crosstab в extension tablefunc.

В MySQL есть PIVOT?

Нет нативного. Только через CASE + GROUP BY.

Что быстрее: PIVOT через CASE или нативный PIVOT?

Обычно сопоставимо. Используйте тот, что удобнее для чтения.

Можно ли PIVOT с динамическим набором колонок?

Только через dynamic SQL (формируете запрос как строку, потом EXECUTE). Громоздко, но работает.


Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов аналитиков.