Learning Platform
Урок 07.04 · 17 мин
Средний
GROUPING SETSROLLUPCUBEGROUPING functionMulti-dimensional aggregation

Зачем сразу несколько разрезов

Допустим, нужен отчёт: «выручка по странам, выручка по годам, и общая выручка». Это три отдельных GROUP BY на одних и тех же данных. Можно написать три запроса и склеить через UNION ALL. Можно один — через GROUPING SETS. Второй вариант короче, обычно быстрее (один проход по данным вместо трёх) и сразу даёт отчёт в нужной форме.

В стандарте SQL для multi-level агрегации есть три конструкции: GROUPING SETS, ROLLUP, CUBE. Все три — это расширения обычного GROUP BY, и все три эквивалентны друг другу через UNION ALL — просто синтаксис компактнее.

GROUPING SETS: явный список разрезов

GROUPING SETS
принимает список наборов колонок. Для каждого набора СУБД делает отдельную группировку и склеивает все результаты в один.

GROUPING SETS = объединение нескольких группировок

Один запрос с GROUPING SETS ((country), (birth_year), ()) эквивалентен трём GROUP BY, склеенным через UNION ALL.

GROUP BY countryстроки по странам
GROUP BY birth_yearстроки по годам
GROUP BY ()одна строка-итог
GROUPING SETS
один результатвсе строки трёх группировок
разрезы помечены NULL в неактивной колонке

Запустим:

Три разреза одним запросом: по country, по birth_year, итог:

PostgreSQL

Что мы видим в результате:

  • Строки, где country заполнен, а birth_year = NULL — это разрез «по странам». NULL в birth_year означает «не группировали по году».
  • Строки, где birth_year заполнен, а country = NULL — разрез «по году».
  • Строка, где оба NULL — общий итог (одна группа на всю таблицу, GROUP BY ()).

Проблема: «настоящий NULL» vs «NULL-маркер группировки»

В наших данных у Ирины birth_year = IS NULL. В результате GROUPING SETS мы тоже видим NULL в birth_year. Как отличить «не знаем год рождения» от «не группировали по году»?

Для этого есть специальная функция

GROUPING(col)
. Она возвращает 1, если колонка в данной строке результата не группировалась (NULL — маркер), и 0, если группировалась (NULL — настоящие данные).

GROUPING() разделяет настоящие NULL и маркер группировки:

PostgreSQL

Теперь легко прочитать:

  • g_country=0, g_year=1 — это разрез «по странам». В birth_year стоит маркер NULL, означающий «эта колонка не активна».
  • g_country=1, g_year=0 — разрез «по годам». В country маркер NULL.
  • g_country=1, g_year=1 — общий итог. Обе колонки не активны.
  • Если бы внутри разреза по году нашёлся настоящий NULL (наша Ирина), у неё было бы g_year=0 и birth_year=NULL — это строка с реальными NULL-данными в составе группы.

Часто полезно завернуть это в CASE:

Симпатичный отчёт с подписями разрезов:

PostgreSQL

ROLLUP: иерархия с подытогами

ROLLUP — это GROUPING SETS с иерархическими подытогами. ROLLUP (a, b, c) означает: «дай мне разрезы (a, b, c), (a, b), (a) и ()». То есть «отбрасывай по одной колонке справа налево».

ROLLUP (country, birth_year)

Эквивалентно GROUPING SETS ((country, birth_year), (country), ()) — иерархия подытогов от самого детального к общему итогу.

ROLLUP (country, birth_year)
=
детально(country, birth_year)
подытог(country) — все года в стране
итог() — весь мир

ROLLUP по (country, birth_year) — детально, подытог по стране, общий итог:

PostgreSQL

Это идеальная форма для отчёта вида «по стране, с подытогами и общим итогом» — типичный финансовый или маркетинговый отчёт.

CUBE: все возможные комбинации

CUBE (a, b, c) — это GROUPING SETS со всеми возможными подмножествами аргументов. То есть для CUBE (a, b) это (a, b), (a), (b), (). Для трёх колонок — 2³ = 8 подмножеств.

CUBE по (country, birth_year) — четыре варианта группировки:

PostgreSQL

Сравни с ROLLUP: у ROLLUP нет строк типа (NULL, 1990) — потому что в иерархии «country → birth_year» нельзя «спрыгнуть» через country. CUBE даёт все комбинации, включая такие.

Когда что использовать. ROLLUP — когда у тебя естественная иерархия (страна → город → район; год → месяц → день). CUBE — когда хочешь dashboard «по любому срезу из этих измерений». GROUPING SETS — когда нужны произвольные конкретные разрезы, а не их декартово произведение.

Производительность: один проход, не три

Если бы ты написал три отдельных запроса с UNION ALL, PostgreSQL прошёл бы по таблице три раза. С GROUPING SETS он делает это за один проход с использованием специального оператора плана MixedAggregate или HashAggregate с несколькими наборами ключей. На большой таблице разница может быть 3–5×.

Проверь сам через EXPLAIN:

EXPLAIN покажет MixedAggregate — один проход на все три разреза:

PostgreSQL

Ты увидишь что-то вроде MixedAggregate или HashAggregate Group Key: с тремя ключами. Это значит — один Seq Scan + одна агрегация в памяти.

Тонкость: ORDER BY и сортировка подытогов

Подытоги (строки с NULL-маркерами) часто хочется ставить последними в группе или первыми. По умолчанию ORDER BY ставит NULL в конец для ASC. Часто пишут ORDER BY GROUPING(col), col, чтобы сначала шли детальные строки группы, а в конце — её подытог.

Сортировка: детальные строки сверху, подытоги снизу:

PostgreSQL
Проверка знанийKnowledge check
Сколько строк примерно вернёт CUBE (country, birth_year) на нашей таблице customers (12 строк, 5 уникальных стран, 11 уникальных годов плюс один NULL)? И как это разложить на GROUPING SETS?
ОтветAnswer
CUBE (country, birth_year) = GROUPING SETS ((country, birth_year), (country), (birth_year), ()). Это даёт: (country, birth_year) — до 12 уникальных пар (вероятно меньше, но в нашем датасете каждый клиент уникален); (country) — 5 строк; (birth_year) — 12 строк (включая NULL); () — 1 строка итога. Итого порядка 12 + 5 + 12 + 1 = 30 строк. Точное число зависит от того, сколько уникальных (country, birth_year) пар в данных.
PIVOT, UNPIVOT и GROUPING SETS в DuckDB WITH ROLLUP, CUBE и TOTALS в ClickHouse

Чек-лист

  • GROUPING SETS ((a), (b), ()) — явный список разрезов. Эквивалентно UNION ALL трёх GROUP BY, но один проход по данным.
  • ROLLUP (a, b, c) = GROUPING SETS ((a,b,c), (a,b), (a), ()). Иерархические подытоги.
  • CUBE (a, b) = все 2² подмножества. Все комбинации измерений.
  • Функция GROUPING(col) возвращает 1, если колонка в этой строке — маркер группировки (не активна), и 0, если активна. Используй для отличения «настоящий NULL» от «NULL-маркер».
  • Один запрос с GROUPING SETS обычно эффективнее, чем UNION ALL нескольких группировок — оптимизатор делает один проход.
  • При сортировке подытогов используй ORDER BY GROUPING(col) для контроля их позиции.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чему эквивалентен ROLLUP (a, b, c) в терминах GROUPING SETS?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6