sqlpivotpostgresqlclickhouseаналитика данных

PIVOT в SQL: строки в столбцы (сводная таблица)

2026-07-10 12 мин

Коротко: PIVOT в SQL — это разворот строк в столбцы, когда значения одного поля (например, месяцы) становятся заголовками колонок, а в ячейках лежит агрегат (сумма, количество). В PostgreSQL и ClickHouse нативного оператора PIVOT нет, поэтому сводную собирают вручную: SUM(CASE WHEN month = 'янв' THEN revenue END) для каждой колонки, либо через более чистый SUM(revenue) FILTER (WHERE month = 'янв') в PostgreSQL. Обратная операция (UNPIVOT — столбцы обратно в строки) делается через UNION ALL или LATERAL.

Зачем аналитику превращать строки в столбцы?

Базы данных хранят данные в «длинном» формате (long / narrow): одна строка — один факт. Продажа категории «Электроника» в марте — это одна строка (category, month, revenue). Это удобно для записи, индексов и агрегаций, но нечитаемо для человека, который хочет увидеть картину «категории по вертикали, месяцы по горизонтали».

Сводная таблица (pivot, cross-tab) переводит данные в «широкий» формат (wide), где каждое уникальное значение измерения становится отдельной колонкой. Именно так выглядит отчёт в Excel-сводной или в BI-дашборде.

Типичные задачи, где нужен разворот:

На собеседованиях аналитика задача «разверни строки в столбцы» встречается регулярно — это проверка на знание агрегатов с условием. Если готовишься, загляни в подборку топ-50 SQL-вопросов на собеседовании.

Почему в PostgreSQL и ClickHouse нет оператора PIVOT?

Оператор PIVOT есть в SQL Server, Oracle и некоторых версиях диалектов, но это не часть стандарта ANSI SQL. PostgreSQL и ClickHouse решили не тащить его в ядро по простой причине: PIVOT требует, чтобы список колонок был известен заранее, а SQL по своей природе работает с фиксированной схемой результата. Движок должен знать имена и типы всех колонок до выполнения запроса. Если ты пивотишь по месяцам, а в данных появился новый месяц — запрос про него не узнает, пока ты руками не допишешь колонку.

Поэтому «настоящий» динамический PIVOT (когда колонки берутся из данных) всегда упирается либо в генерацию SQL строкой, либо в расширения:

Хорошая новость: в 90% реальных отчётов список колонок известен (12 месяцев, 7 дней недели, 4 квартала), и статический pivot через CASE/FILTER полностью решает задачу — переносимо между всеми диалектами. Именно на нём и сосредоточимся.

Как собрать сводную через CASE WHEN + SUM?

Базовый приём: для каждой будущей колонки пишем агрегат с условием внутри. CASE WHEN возвращает значение только для нужного месяца, иначе NULL, а SUM игнорирует NULL — так суммируется только «своя» ячейка.

Пусть есть таблица продаж в длинном формате:

-- sales: (category TEXT, sale_month TEXT, revenue NUMERIC)
-- 'Электроника' | '2026-01' | 120000
-- 'Электроника' | '2026-02' | 145000
-- 'Одежда'      | '2026-01' |  80000
-- ...

Разворачиваем месяцы в колонки:

SELECT
    category,
    SUM(CASE WHEN sale_month = '2026-01' THEN revenue ELSE 0 END) AS jan,
    SUM(CASE WHEN sale_month = '2026-02' THEN revenue ELSE 0 END) AS feb,
    SUM(CASE WHEN sale_month = '2026-03' THEN revenue ELSE 0 END) AS mar,
    SUM(revenue) AS total
FROM sales
WHERE sale_month BETWEEN '2026-01' AND '2026-03'
GROUP BY category
ORDER BY total DESC;

Результат:

categoryjanfebmartotal
Электроника120000145000160000425000
Одежда800009200088000260000
Книги15000140001900048000

Что здесь важно понять:

Тот же приём работает для COUNT (сколько заказов), AVG (средний чек) или MAX. Например, количество заказов вместо выручки — просто COUNT(CASE WHEN sale_month = '2026-01' THEN 1 END).

Хочешь освежить, как работают GROUP BY и агрегаты под капотом — есть разбор GROUP BY и HAVING: порядок выполнения и базовая часть про агрегаты в курсе SQL с нуля.

Чем FILTER (WHERE ...) удобнее CASE в PostgreSQL?

PostgreSQL (и ClickHouse, и SQLite новых версий) поддерживает стандартный ANSI-синтаксис FILTER, который делает то же самое, но читается заметно чище:

SELECT
    category,
    SUM(revenue) FILTER (WHERE sale_month = '2026-01') AS jan,
    SUM(revenue) FILTER (WHERE sale_month = '2026-02') AS feb,
    SUM(revenue) FILTER (WHERE sale_month = '2026-03') AS mar,
    SUM(revenue) AS total
FROM sales
WHERE sale_month BETWEEN '2026-01' AND '2026-03'
GROUP BY category
ORDER BY total DESC;

Три преимущества FILTER перед CASE:

Важная деталь: CASE WHEN переносим между ВСЕМИ диалектами SQL без исключений, а FILTER поддерживается в PostgreSQL, ClickHouse, SQLite ≥ 3.30 и в стандарте, но, например, в MySQL до недавних версий его не было. Если пишешь запрос, который должен работать где угодно, — бери CASE. Если работаешь в Postgres или ClickHouse — FILTER предпочтительнее.

В ClickHouse есть ещё третий, самый короткий вариант — комбинатор -If:

SELECT
    category,
    sumIf(revenue, sale_month = '2026-01') AS jan,
    sumIf(revenue, sale_month = '2026-02') AS feb,
    sumIf(revenue, sale_month = '2026-03') AS mar
FROM sales
GROUP BY category;
sumIf, countIf, avgIf — идиоматичный ClickHouse-способ. Подробнее про особенности движка — в практическом гайде по ClickHouse.

Как построить выручку по категориям × месяцам одним запросом?

Соберём отчёт целиком, как его ждёт финансовый менеджер: категории по строкам, месяцы по колонкам, плюс итоговая колонка и доля категории в общей выручке. Возьмём полный год, чтобы было наглядно, и покажем компактную версию на четыре квартала.

WITH monthly AS (
    SELECT
        category,
        EXTRACT(QUARTER FROM sale_date)::int AS q,
        revenue
    FROM sales
    WHERE sale_date >= DATE '2026-01-01'
      AND sale_date <  DATE '2027-01-01'
)
SELECT
    category,
    SUM(revenue) FILTER (WHERE q = 1) AS q1,
    SUM(revenue) FILTER (WHERE q = 2) AS q2,
    SUM(revenue) FILTER (WHERE q = 3) AS q3,
    SUM(revenue) FILTER (WHERE q = 4) AS q4,
    SUM(revenue) AS total_year,
    ROUND(
        100.0 * SUM(revenue) / SUM(SUM(revenue)) OVER (),
        1
    ) AS share_pct
FROM monthly
GROUP BY category
ORDER BY total_year DESC;

Разбор нетривиальных мест:

Пример результата:

categoryq1q2q3q4total_yearshare_pct
Электроника425000480000510000690000210500047.8
Одежда260000310000290000420000128000029.1
Книги480005200061000950002560005.8

Такой отчёт связывается с продуктовыми метриками напрямую: total_year по категориям складывается в общий GMV маркетплейса, а средняя ячейка по строке — это грубая прокси среднего чека AOV в динамике. Про разбор такого кейса целиком — маркетплейс: GMV и take rate.

Хочешь потрогать этот запрос на живых данных — открой SQL-тренажёр: там настоящий PostgreSQL 16 в браузере, можно менять FILTER на CASE и сравнивать план выполнения.

Как выполнить обратную операцию UNPIVOT?

UNPIVOT — разворот в обратную сторону: широкая таблица (месяцы-колонки) снова превращается в длинную (одна строка на месяц). Это нужно, когда данные пришли уже сводными (выгрузка из Excel, витрина с колонками jan, feb, mar), а тебе для анализа, джойнов или загрузки в BI требуется нормальный long-формат.

Нативного UNPIVOT в PostgreSQL тоже нет. Два рабочих способа.

Способ 1 — UNION ALL. Прямолинейно и переносимо в любой диалект:

SELECT category, '2026-01' AS sale_month, jan AS revenue FROM wide_sales
UNION ALL
SELECT category, '2026-02', feb FROM wide_sales
UNION ALL
SELECT category, '2026-03', mar FROM wide_sales;

Минус очевиден: таблица wide_sales сканируется столько раз, сколько колонок разворачиваешь. Для трёх месяцев ок, для 12 — уже расточительно.

Способ 2 — LATERAL + VALUES (PostgreSQL). Таблица сканируется один раз, а каждая строка «размножается» на нужное число:

SELECT
    w.category,
    v.sale_month,
    v.revenue
FROM wide_sales w
CROSS JOIN LATERAL (
    VALUES
        ('2026-01', w.jan),
        ('2026-02', w.feb),
        ('2026-03', w.mar)
) AS v(sale_month, revenue)
WHERE v.revenue IS NOT NULL;

В ClickHouse обратный разворот делают через arrayJoin: складываешь значения в массив и «взрываешь» его в строки — тоже один проход по таблице.

Результат UNPIVOT для обоих способов одинаковый:

categorysale_monthrevenue
Электроника2026-01120000
Электроника2026-02145000
Электроника2026-03160000

Длинный формат после UNPIVOT удобнее для дальнейшей аналитики: по нему легко считать оконные функции, join'ить с другими таблицами и строить нарастающий итог.

Какие ошибки чаще всего ломают SQL-сводную?

Разворот строк в столбцы выглядит просто, но именно на нём новички теряют баллы на собеседовании и время в бою. Вот типичные грабли, на которые лучше наступить заранее, читая статью, чем вживую на демонстрации результата заказчику.

Хорошая привычка — сначала написать длинный SELECT с GROUP BY, глазами убедиться, что цифры сходятся, и только потом разворачивать его в широкую таблицу. Отладить агрегат проще на длинном формате, чем ловить, почему одна из двенадцати колонок врёт.

Когда лучше не пивотить в SQL, а отдать это BI или pandas?

SQL-pivot через CASE/FILTER хорош, когда колонок мало и они известны заранее. Но у него есть предел, за которым инструмент выбран неправильно.

Не пивоть в SQL, а передай задачу дальше, если:

Простое правило выбора инструмента:

СитуацияИнструмент
Мало фиксированных колонок, результат — таблица/выгрузкаSQL: CASE / FILTER
Динамические колонки, финал — дашбордBI (разворот в интерфейсе)
Нужны проценты, ranking, гибкая обработкаpandas pivot_table
Пришли широкие данные, нужен long для анализаSQL UNPIVOT (UNION ALL / LATERAL)

Мой практический совет: держи данные в длинном формате как можно дольше по пайплайну, а разворачивай в широкий только на самом последнем шаге — там, где их читает человек. Так проще джойнить, агрегировать и тестировать. Пивот — это про презентацию, а не про хранение.

Что запомнить

Разворот строк в столбцы — из тех приёмов, которые нужно набить руками, чтобы они всплывали на собеседовании автоматически. В SQL-тренажёре собраны задачи именно на сводные, воронки и когорты: первые пять решаешь бесплатно и без регистрации, дальше — по подписке. Хочешь проверить себя в формате диалога — прогони AI-мок-собеседование или отработай смежные темы в банке вопросов для аналитика. А если тянет к Python — те же данные можно развернуть в Python-тренажёре через pivot_table. Начать системно с азов удобнее с курса SQL с нуля.

Отработай PIVOT на реальных задачах
425 SQL-задач с проверкой в браузере: PostgreSQL 16 и SQLite. Первые 5 задач бесплатно, без регистрации — попробуй собрать сводную своими руками.
Открыть SQL-тренажёр →