sqlpostgresqlrollupgrouping-sets

ROLLUP en SQL: subtotales jerarquicos y un total general en una sola consulta

Como GROUP BY ROLLUP anade subtotales y una fila de total general a la agregacion normal, y como leer los NULL finales con GROUPING().

9 min de lecturaReferencesql · postgresql · rollup · grouping-sets · aggregation · reporting
Este artículo está actualmente en ruso — la traducción está en curso.

Обычный GROUP BY считает агрегаты на одном уровне.

Например, можно посчитать выручку по странам:

SELECT
  country,
  SUM(amount) AS revenue
FROM orders
GROUP BY country;

Результат может быть таким:

country | revenue
--------+--------
Germany | 2300
Vietnam | 5000
Spain   | 900

Можно посчитать выручку по странам и статусам заказов:

SELECT
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY country, status;

Результат:

country | status   | revenue
--------+----------+--------
Germany | paid     | 2300
Vietnam | paid     | 5000
Vietnam | refunded | 900
Spain   | failed   | 700

Но в отчётах часто нужно не только это.

Обычно хочется увидеть:

  • детализацию по стране и статусу;
  • подытог по каждой стране;
  • общий итог по всему отчёту.

Например:

country | status       | revenue
--------+--------------+--------
Germany | paid         | 2300
Germany | ALL STATUSES | 2300
Vietnam | paid         | 5000
Vietnam | refunded     | 900
Vietnam | ALL STATUSES | 5900
Spain   | failed       | 700
Spain   | ALL STATUSES | 700
ALL     | ALL          | 8900

Такой отчёт можно собрать несколькими запросами через UNION ALL.

Но в SQL есть специальный инструмент для таких задач — ROLLUP.

Он позволяет добавить подытоги и общий итог прямо в GROUP BY.

Что такое ROLLUP простыми словами

ROLLUP — это расширение GROUP BY, которое строит агрегаты на нескольких уровнях детализации.

Например:

GROUP BY ROLLUP (country, status)

означает:

1. Сначала сгруппируй по country и status.
2. Потом сделай подытог по country.
3. Потом сделай общий итог по всему набору.

То есть ROLLUP (country, status) создаёт такие уровни:

(country, status)
(country)
()

Пустые скобки () означают общий итог без группировки по колонкам.

Если объяснить совсем просто:

ROLLUP берёт обычный отчёт и добавляет к нему подытоги снизу вверх.

Базовый пример ROLLUP

Допустим, есть таблица orders:

id | country | status   | amount
---+---------+----------+--------
1  | Vietnam | paid     | 1500
2  | Vietnam | paid     | 2300
3  | Vietnam | refunded | 900
4  | Germany | paid     | 2000
5  | Spain   | failed   | 700

Хотим посчитать выручку:

  • по стране и статусу;
  • по стране целиком;
  • общий итог.

Запрос:

SELECT
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status);

Результат может быть таким:

country | status   | revenue
--------+----------+--------
Germany | paid     | 2000
Germany | NULL     | 2000
Spain   | failed   | 700
Spain   | NULL     | 700
Vietnam | paid     | 3800
Vietnam | refunded | 900
Vietnam | NULL     | 4700
NULL    | NULL     | 7400

Что здесь происходит?

Строки, где заполнены и country, и status, — это обычная детализация.

Vietnam | paid     | 3800
Vietnam | refunded | 900

Строка, где country заполнен, а status = NULL, — это подытог по стране.

Vietnam | NULL | 4700

Строка, где и country, и status равны NULL, — это общий итог.

NULL | NULL | 7400

На уровне механики всё работает. Но для реального отчёта такой результат пока опасен: непонятно, где настоящий NULL, а где подытог.

Почему в итоговых строках появляются NULL

Когда ROLLUP считает подытог, часть колонок перестаёт участвовать в группировке.

Например, для строки:

Vietnam | NULL | 4700

это не значит, что у заказов статус реально NULL.

Это значит:

это итог по всем статусам внутри Vietnam.

То есть status здесь свернут.

А строка:

NULL | NULL | 7400

означает:

это общий итог по всем странам и всем статусам.

Проблема в том, что NULL может быть и настоящим значением в данных.

Например, в таблице действительно может быть заказ без статуса:

country | status | amount
--------+--------+--------
Vietnam | NULL   | 500

Тогда в результате появится строка с status = NULL, и глазами будет сложно понять:

  • это заказ без статуса;
  • или подытог по всем статусам.

Именно поэтому с ROLLUP почти всегда нужно использовать функцию GROUPING().

GROUPING(): как отличить подытог от настоящего NULL

Функция GROUPING(column) показывает, была ли колонка свернута на текущем уровне агрегации.

Она возвращает:

0 — колонка участвует в группировке
1 — колонка свернута, то есть это итоговый уровень

Пример:

SELECT
  GROUPING(country) AS g_country,
  GROUPING(status) AS g_status,
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status)
ORDER BY
  GROUPING(country),
  country,
  GROUPING(status),
  status;

Результат может выглядеть так:

g_country | g_status | country | status   | revenue
----------+----------+---------+----------+--------
0         | 0        | Germany | paid     | 2000
0         | 1        | Germany | NULL     | 2000
0         | 0        | Spain   | failed   | 700
0         | 1        | Spain   | NULL     | 700
0         | 0        | Vietnam | paid     | 3800
0         | 0        | Vietnam | refunded | 900
0         | 1        | Vietnam | NULL     | 4700
1         | 1        | NULL    | NULL     | 7400

Теперь всё понятно.

Если:

GROUPING(status) = 1

значит status не участвует в текущем уровне. Это подытог по всем статусам.

Если:

GROUPING(status) = 0

значит status участвует в группировке. Если при этом сам status равен NULL, значит это настоящий NULL из данных.

Главное правило:

GROUPING(col) = 1 не означает «в колонке NULL». Это означает «колонка свернута в итоговой строке».

Это очень важное различие.

Делаем отчёт читаемым: подписи вместо NULL

В реальном отчёте лучше сразу заменить итоговые NULL на понятные подписи.

Например:

SELECT
  CASE
    WHEN GROUPING(country) = 1 THEN 'ALL COUNTRIES'
    WHEN country IS NULL THEN 'NO COUNTRY'
    ELSE country
  END AS country_label,
  CASE
    WHEN GROUPING(status) = 1 THEN 'ALL STATUSES'
    WHEN status IS NULL THEN 'NO STATUS'
    ELSE status
  END AS status_label,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status)
ORDER BY
  GROUPING(country),
  country_label,
  GROUPING(status),
  status_label;

Результат:

country_label | status_label | revenue
--------------+--------------+--------
Germany       | paid         | 2000
Germany       | ALL STATUSES | 2000
Spain         | failed       | 700
Spain         | ALL STATUSES | 700
Vietnam       | paid         | 3800
Vietnam       | refunded     | 900
Vietnam       | ALL STATUSES | 4700
ALL COUNTRIES | ALL STATUSES | 7400

Теперь результат можно отдавать в BI, CSV или API без объяснения, что «пустой статус иногда значит все статусы».

Также мы отдельно обработали реальные NULL:

WHEN status IS NULL THEN 'NO STATUS'

Это важно: настоящий пропуск в данных и итоговая строка — разные вещи.

Порядок колонок в ROLLUP важен

ROLLUP сворачивает уровни справа налево.

Например:

ROLLUP (country, status)

создаёт уровни:

(country, status)
(country)
()

То есть:

  • детализация по стране и статусу;
  • подытог по стране;
  • общий итог.

А вот:

ROLLUP (status, country)

создаёт другие уровни:

(status, country)
(status)
()

Теперь подытог будет по статусу, а не по стране.

Это уже другой отчёт.

Первый вариант отвечает на вопрос:

сколько выручки по статусам внутри каждой страны?

Второй вариант отвечает на вопрос:

сколько выручки по странам внутри каждого статуса?

Поэтому порядок колонок в ROLLUP — это не косметика. Он задаёт иерархию отчёта.

Можно запомнить так:

В ROLLUP (a, b, c) детализация идёт слева направо, а сворачивание — справа налево.

Для трёх колонок:

ROLLUP (year, month, day)

создаст:

(year, month, day)
(year, month)
(year)
()

Это естественная иерархия:

год → месяц → день

ROLLUP для отчёта «месяц → страна»

Теперь разберём более реальный пример.

Есть таблица orders и таблица users.

orders:

id | user_id | amount | status | created_at
---+---------+--------+--------+---------------------
1  | 1       | 1500   | paid   | 2026-06-01 10:00:00
2  | 2       | 2300   | paid   | 2026-06-05 12:00:00
3  | 3       | 900    | paid   | 2026-07-02 09:00:00

users:

id | country
---+---------
1  | Vietnam
2  | Germany
3  | Vietnam

Хотим отчёт по оплаченной выручке:

  • по месяцу и стране;
  • подытог по месяцу;
  • общий итог за весь период.

Запрос:

SELECT
  CASE
    WHEN GROUPING(DATE_TRUNC('month', o.created_at)) = 1 THEN 'ALL MONTHS'
    ELSE TO_CHAR(DATE_TRUNC('month', o.created_at), 'YYYY-MM')
  END AS month_label,
  CASE
    WHEN GROUPING(u.country) = 1 THEN 'ALL COUNTRIES'
    WHEN u.country IS NULL THEN 'NO COUNTRY'
    ELSE u.country
  END AS country_label,
  SUM(o.amount) AS revenue
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'paid'
GROUP BY ROLLUP (
  DATE_TRUNC('month', o.created_at),
  u.country
)
ORDER BY
  GROUPING(DATE_TRUNC('month', o.created_at)),
  DATE_TRUNC('month', o.created_at),
  GROUPING(u.country),
  u.country;

Результат может быть таким:

month_label | country_label | revenue
------------+---------------+--------
2026-06     | Germany       | 2300
2026-06     | Vietnam       | 1500
2026-06     | ALL COUNTRIES | 3800
2026-07     | Vietnam       | 900
2026-07     | ALL COUNTRIES | 900
ALL MONTHS  | ALL COUNTRIES | 4700

Здесь ROLLUP строит уровни:

(month, country)
(month)
()

То есть:

  • страны внутри месяца;
  • итог по каждому месяцу;
  • общий итог.

Это классический drill-down отчёт.

Важно: выражение в SELECT и GROUP BY должно совпадать

В примере выше мы группируем не просто по колонке, а по выражению:

DATE_TRUNC('month', o.created_at)

Если вы используете выражение в GROUP BY, то в SELECT и GROUPING() нужно ссылаться на то же самое выражение.

Например:

GROUPING(DATE_TRUNC('month', o.created_at))

а не:

GROUPING(o.created_at)

Почему?

Потому что уровень группировки построен именно по месяцу, а не по исходному timestamp.

Без этого можно получить ошибку или неправильную логику отчёта.

Для читаемости в сложных запросах можно вынести выражения в CTE.

WITH prepared_orders AS (
  SELECT
    o.id,
    o.amount,
    DATE_TRUNC('month', o.created_at) AS order_month,
    u.country
  FROM orders o
  JOIN users u ON u.id = o.user_id
  WHERE o.status = 'paid'
)
SELECT
  CASE
    WHEN GROUPING(order_month) = 1 THEN 'ALL MONTHS'
    ELSE TO_CHAR(order_month, 'YYYY-MM')
  END AS month_label,
  CASE
    WHEN GROUPING(country) = 1 THEN 'ALL COUNTRIES'
    WHEN country IS NULL THEN 'NO COUNTRY'
    ELSE country
  END AS country_label,
  SUM(amount) AS revenue
FROM prepared_orders
GROUP BY ROLLUP (order_month, country)
ORDER BY
  GROUPING(order_month),
  order_month,
  GROUPING(country),
  country;

Так запрос становится проще читать: мы сначала готовим поля, а потом строим отчёт.

Стабильная сортировка итогов

Без ORDER BY порядок строк в SQL не гарантирован.

Это особенно важно для ROLLUP, потому что в результате смешиваются:

  • детальные строки;
  • подытоги;
  • общий итог.

Если не задать порядок явно, база может вернуть строки в удобном для себя порядке.

Для отчёта это плохо. Сегодня подытог может оказаться под деталями, завтра — выше них.

Поэтому сортировку лучше строить через GROUPING().

Например:

ORDER BY
  GROUPING(country),
  country,
  GROUPING(status),
  status;

Идея такая:

  • сначала строки, где country участвует в группировке;
  • потом общий итог по странам;
  • внутри страны сначала детальные статусы;
  • потом итог по всем статусам.

Для человекочитаемых отчётов часто нужно, чтобы подытог шёл после деталей.

Если нужна более гибкая сортировка, можно добавить техническую колонку:

CASE
  WHEN GROUPING(country) = 0 AND GROUPING(status) = 0 THEN 1
  WHEN GROUPING(country) = 0 AND GROUPING(status) = 1 THEN 2
  WHEN GROUPING(country) = 1 AND GROUPING(status) = 1 THEN 3
END AS row_level_sort

И потом сортировать по ней.

Добавляем row_level: уровень строки

Для API или BI часто полезно явно указать уровень строки.

Например:

SELECT
  CASE
    WHEN GROUPING(country) = 0 AND GROUPING(status) = 0 THEN 'detail'
    WHEN GROUPING(country) = 0 AND GROUPING(status) = 1 THEN 'country_total'
    WHEN GROUPING(country) = 1 AND GROUPING(status) = 1 THEN 'grand_total'
  END AS row_level,
  CASE
    WHEN GROUPING(country) = 1 THEN 'ALL COUNTRIES'
    WHEN country IS NULL THEN 'NO COUNTRY'
    ELSE country
  END AS country_label,
  CASE
    WHEN GROUPING(status) = 1 THEN 'ALL STATUSES'
    WHEN status IS NULL THEN 'NO STATUS'
    ELSE status
  END AS status_label,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status)
ORDER BY
  CASE
    WHEN GROUPING(country) = 0 AND GROUPING(status) = 0 THEN 1
    WHEN GROUPING(country) = 0 AND GROUPING(status) = 1 THEN 2
    WHEN GROUPING(country) = 1 AND GROUPING(status) = 1 THEN 3
  END,
  country_label,
  status_label;

Результат:

row_level     | country_label | status_label | revenue
--------------+---------------+--------------+--------
detail        | Germany       | paid         | 2000
detail        | Spain         | failed       | 700
detail        | Vietnam       | paid         | 3800
detail        | Vietnam       | refunded     | 900
country_total | Germany       | ALL STATUSES | 2000
country_total | Spain         | ALL STATUSES | 700
country_total | Vietnam       | ALL STATUSES | 4700
grand_total   | ALL COUNTRIES | ALL STATUSES | 7400

Теперь downstream-коду не нужно угадывать, какая строка является подытогом.

Это хороший паттерн для регулярных отчётов.

ROLLUP — это сокращение для GROUPING SETS

ROLLUP не отдельная магия. Это короткая запись для GROUPING SETS.

Например:

GROUP BY ROLLUP (country, status)

эквивалентно:

GROUP BY GROUPING SETS (
  (country, status),
  (country),
  ()
)

То есть ROLLUP просто автоматически строит иерархию уровней.

Для трёх колонок:

GROUP BY ROLLUP (year, month, day)

это то же самое, что:

GROUP BY GROUPING SETS (
  (year, month, day),
  (year, month),
  (year),
  ()
)

Если вам нужна именно такая иерархия, ROLLUP короче и удобнее.

Если же нужны нестандартные уровни, лучше использовать GROUPING SETS.

Когда использовать ROLLUP, а когда GROUPING SETS

ROLLUP хорош для естественных иерархий.

Например:

год → месяц → день
страна → регион → город
категория → подкатегория → товар
отдел → менеджер → сотрудник

В таких случаях подытоги имеют понятный смысл.

Например:

GROUP BY ROLLUP (country, city)

даёт:

(country, city)
(country)
()

То есть города внутри страны, итог по стране и общий итог.

А вот если измерения независимы, ROLLUP может быть не тем, что нужно.

Например:

country
payment_method

Страна и способ оплаты не всегда образуют естественную иерархию. Если написать:

GROUP BY ROLLUP (country, payment_method)

вы получите:

(country, payment_method)
(country)
()

То есть будет итог по стране, но не будет отдельного итога по способу оплаты.

Если вам нужны и итоги по странам, и итоги по способам оплаты, и общий итог, лучше использовать GROUPING SETS или CUBE.

Например:

GROUP BY GROUPING SETS (
  (country, payment_method),
  (country),
  (payment_method),
  ()
)

Или:

GROUP BY CUBE (country, payment_method)

если нужны все комбинации.

ROLLUP против CUBE

ROLLUP строит иерархию.

ROLLUP (a, b)

создаёт:

(a, b)
(a)
()

CUBE строит все комбинации.

CUBE (a, b)

создаёт:

(a, b)
(a)
(b)
()

Разница важная.

Если у вас отчёт:

страна → город

логичнее ROLLUP.

Если у вас независимые измерения:

страна и статус оплаты

и нужны итоги по каждому измерению отдельно, может подойти CUBE.

Можно запомнить так:

ROLLUP — иерархия с подытогами. CUBE — все комбинации измерений. GROUPING SETS — вручную выбранные уровни.

ROLLUP с HAVING

HAVING можно использовать вместе с ROLLUP.

Например, оставить только строки, где выручка больше 1000:

SELECT
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status)
HAVING SUM(amount) > 1000;

Но здесь нужно быть осторожным.

HAVING применится ко всем уровням:

  • к детальным строкам;
  • к подытогам;
  • к общему итогу.

Иногда это именно то, что нужно. А иногда вы хотите фильтровать только детальные строки, но оставить все подытоги.

Тогда используйте GROUPING().

Например:

SELECT
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status)
HAVING
     GROUPING(country) = 1
  OR GROUPING(status) = 1
  OR SUM(amount) > 1000;

Этот запрос оставит итоговые строки и детальные строки с выручкой больше 1000.

Но такие условия быстро становятся сложными. Для регулярных отчётов лучше явно добавлять row_level и аккуратно проектировать фильтрацию.

ROLLUP и WHERE

Обычный WHERE применяется до группировки.

Например:

SELECT
  country,
  status,
  SUM(amount) AS revenue
FROM orders
WHERE created_at >= DATE '2026-06-01'
  AND created_at <  DATE '2026-07-01'
GROUP BY ROLLUP (country, status);

Здесь сначала выбираются только заказы за июнь 2026 года.

А потом по этим строкам строятся:

  • детали по стране и статусу;
  • подытоги по стране;
  • общий итог за июнь.

Это удобно: фильтр по периоду пишется один раз и применяется ко всем уровням отчёта.

Если бы вы делали отчёт через несколько UNION ALL, этот фильтр пришлось бы повторять в каждой ветке.

Почему ROLLUP лучше делать в SQL, а не в приложении

Иногда кажется, что можно просто получить детальные строки, а подытоги посчитать уже в приложении.

Например:

SELECT country, status, SUM(amount)
FROM orders
GROUP BY country, status;

А потом в коде сложить суммы по странам и общий итог.

Так можно, но у этого подхода есть минусы:

  • логика отчёта размазывается между SQL и приложением;
  • сложнее проверять корректность;
  • легко получить расхождения между BI и API;
  • приложение может считать подытоги иначе, чем база;
  • сложнее переиспользовать запрос;
  • больше данных приходится передавать наружу.

ROLLUP позволяет держать логику отчёта в SQL.

База сама знает, как агрегировать данные, и возвращает готовый результат с деталями, подытогами и общим итогом.

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

ROLLUP не означает, что база «бесплатно» добавляет итоги.

Ей всё равно нужно выполнить агрегацию на нескольких уровнях.

Но обычно это лучше, чем писать несколько отдельных запросов с одинаковыми WHERE, JOIN и GROUP BY, а потом склеивать их через UNION ALL.

Например, вместо:

SELECT country, status, SUM(amount)
FROM orders
GROUP BY country, status

UNION ALL

SELECT country, NULL, SUM(amount)
FROM orders
GROUP BY country

UNION ALL

SELECT NULL, NULL, SUM(amount)
FROM orders;

можно написать:

SELECT
  country,
  status,
  SUM(amount)
FROM orders
GROUP BY ROLLUP (country, status);

Так запрос короче, а планировщик может эффективнее работать с одной логикой агрегации.

Но на больших таблицах всё равно стоит проверять план через:

EXPLAIN

или:

EXPLAIN ANALYZE

Особенно если есть сложные JOIN, фильтры, много уровней или тяжёлые агрегаты.

ROLLUP в PostgreSQL

В PostgreSQL можно писать стандартный синтаксис:

GROUP BY ROLLUP (country, status)

Также доступны:

GROUPING SETS
CUBE
GROUPING()

Это делает PostgreSQL удобным для отчётных запросов с несколькими уровнями агрегации.

Пример:

SELECT
  country,
  status,
  SUM(amount) AS revenue,
  GROUPING(country) AS g_country,
  GROUPING(status) AS g_status
FROM orders
GROUP BY ROLLUP (country, status);

Функция GROUPING() особенно важна, если в данных могут быть настоящие NULL.

ROLLUP в MySQL

В MySQL используется другой синтаксис:

GROUP BY country, status WITH ROLLUP

Пример:

SELECT
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY country, status WITH ROLLUP;

Идея похожая: сначала детализация по country, status, потом подытоги и общий итог.

Но возможности отличаются от PostgreSQL.

В MySQL нет такой же стандартной формы GROUP BY ROLLUP (...), и поддержка функций вроде GROUPING() зависит от версии.

Также с сортировкой рядом с WITH ROLLUP нужно быть внимательнее: лучше проверять поведение на конкретной версии MySQL и не полагаться на случайный порядок строк.

ROLLUP в ClickHouse

В ClickHouse тоже есть инструменты для итогов и подытогов, но синтаксис и поведение могут отличаться.

Можно встретить варианты вроде:

GROUP BY country, status WITH ROLLUP

Также в ClickHouse есть WITH CUBE и WITH TOTALS.

Для аналитических отчётов это полезно, но важно помнить:

  • порядок строк без явного ORDER BY не гарантирован;
  • поведение итоговых строк может отличаться от PostgreSQL;
  • конкретные возможности зависят от версии и настроек.

Если вы переносите запросы между PostgreSQL, MySQL и ClickHouse, проверяйте синтаксис и результат на реальных данных.

Практические шаблоны

Подытоги по стране и общий итог

SELECT
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status);

То же самое с флагами GROUPING

SELECT
  GROUPING(country) AS g_country,
  GROUPING(status) AS g_status,
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status);

Читаемые подписи итогов

SELECT
  CASE
    WHEN GROUPING(country) = 1 THEN 'ALL COUNTRIES'
    WHEN country IS NULL THEN 'NO COUNTRY'
    ELSE country
  END AS country_label,
  CASE
    WHEN GROUPING(status) = 1 THEN 'ALL STATUSES'
    WHEN status IS NULL THEN 'NO STATUS'
    ELSE status
  END AS status_label,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status);

Уровень строки для API или BI

SELECT
  CASE
    WHEN GROUPING(country) = 0 AND GROUPING(status) = 0 THEN 'detail'
    WHEN GROUPING(country) = 0 AND GROUPING(status) = 1 THEN 'country_total'
    WHEN GROUPING(country) = 1 AND GROUPING(status) = 1 THEN 'grand_total'
  END AS row_level,
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status);

Отчёт по месяцам и странам

WITH prepared_orders AS (
  SELECT
    DATE_TRUNC('month', o.created_at) AS order_month,
    u.country,
    o.amount
  FROM orders o
  JOIN users u ON u.id = o.user_id
  WHERE o.status = 'paid'
)
SELECT
  CASE
    WHEN GROUPING(order_month) = 1 THEN 'ALL MONTHS'
    ELSE TO_CHAR(order_month, 'YYYY-MM')
  END AS month_label,
  CASE
    WHEN GROUPING(country) = 1 THEN 'ALL COUNTRIES'
    WHEN country IS NULL THEN 'NO COUNTRY'
    ELSE country
  END AS country_label,
  SUM(amount) AS revenue
FROM prepared_orders
GROUP BY ROLLUP (order_month, country)
ORDER BY
  GROUPING(order_month),
  order_month,
  GROUPING(country),
  country;

Иерархия год → месяц → день

SELECT
  EXTRACT(YEAR FROM created_at) AS year,
  EXTRACT(MONTH FROM created_at) AS month,
  EXTRACT(DAY FROM created_at) AS day,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (
  EXTRACT(YEAR FROM created_at),
  EXTRACT(MONTH FROM created_at),
  EXTRACT(DAY FROM created_at)
);

ROLLUP как GROUPING SETS

SELECT
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY GROUPING SETS (
  (country, status),
  (country),
  ()
);

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

ROLLUP добавляет к обычной группировке подытоги и общий итог.

Пример:

SELECT
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status);

ROLLUP (country, status) создаёт уровни:

(country, status)
(country)
()

То есть:

детализация по стране и статусу
подытог по стране
общий итог

Главные правила:

  • порядок колонок в ROLLUP важен;
  • сворачивание идёт справа налево;
  • для n колонок создаётся n + 1 уровень агрегации;
  • итоговые строки получают NULL в свернутых колонках;
  • настоящий NULL из данных и итоговый NULL нужно различать через GROUPING();
  • GROUPING(col) = 1 означает, что колонка свернута;
  • для отчётов лучше добавлять понятные подписи и row_level;
  • порядок строк без ORDER BY не гарантирован;
  • ROLLUP — это сокращение для частого случая GROUPING SETS.

Короткий вывод

ROLLUP нужен, когда отчёт строится по естественной иерархии и требует подытогов.

Например:

страна → статус → выручка
год → месяц → день → сумма
категория → подкатегория → продажи
отдел → менеджер → зарплатный фонд

Вместо нескольких запросов с UNION ALL можно написать один:

SELECT
  country,
  status,
  SUM(amount) AS revenue
FROM orders
GROUP BY ROLLUP (country, status);

Главная мысль:

ROLLUP делает детальные строки, подытоги и общий итог внутри одного GROUP BY.

Но хороший ROLLUP-отчёт — это не только короткий синтаксис.

Нужно заранее продумать:

  • какие уровни нужны;
  • в каком порядке должны идти строки;
  • как подписать подытоги;
  • как отличить настоящий NULL от итоговой строки;
  • что должен получить downstream-код или BI-инструмент.

Если это сделать, ROLLUP превращает сложный отчёт с подытогами в понятный и поддерживаемый SQL-запрос.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador