open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 03 · 04

GROUPING SETS, ROLLUP, CUBE: много группировок за один проход

GROUPING SETS считает несколько группировок одним запросом; ROLLUP добавляет иерархические подытоги, CUBE — все комбинации. GROUPING() отличает NULL подытога от настоящего.

SQL Senior ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

Финансовой команде нужна была одна таблица: выручка по статусу, выручка по пользователю и общий итог — всё сразу. Наивная сборка — три SELECT, склеенные UNION ALL, сканирующие orders трижды и разъезжающиеся, как только кто-то правил одну ветку. Один GROUP BY ROLLUP заменил всё это: один скан, подытоги и общий итог включены, рассинхронизировать невозможно. Это фича, превращающая отчётный запрос из обузы для сопровождения в одну строку.

Один запрос, несколько группировок

Спроси себя: сколько раз ты видел дашборд, собранный из трёх почти одинаковых запросов на UNION ALL, каждый из которых сканирует одну таблицу и рассинхронизируется, как только кто-то правит баг только в одной ветке? GROUPING SETS — ответ на этот вопрос.

Обычно GROUP BY выдаёт ровно одну группировку. GROUPING SETS позволяет запросить несколько группировок одним запросом, складывая их результаты в один вывод:

SELECT status, user_id, SUM(total) AS revenue
FROM orders
GROUP BY GROUPING SETS (
  (status),        -- выручка по статусу
  (user_id),       -- выручка по пользователю
  ()               -- общий итог (пустое множество = без группировки)
);

Каждый список в скобках — одна группировка. Пустое множество () значит «не группировать вовсе» — общий итог по всем строкам. Postgres сканирует orders один раз и выдаёт объединение всех трёх группировок. Колонки, не входящие в данную группировку, возвращаются как NULL в тех строках (user_id равен NULL в строках по статусу, ведь пользователя нет в этом множестве).

ROLLUP и CUBE: сокращения для частых форм

Два сокращения покрывают шаблоны, которые тебе реально нужны:

  • ROLLUP (a, b) разворачивается в иерархические множества группировок (a, b), (a), (). Используй, когда колонки образуют иерархию — (year, month) → итоги по месяцу, подытог по году, общий итог. Это «drill-down с подытогами».
  • CUBE (a, b) разворачивается во все комбинации: (a, b), (a), (b), (). Используй для полной кросс-таблицы, где важны все измерения и итоги.
-- Подытоги по статусу в иерархии rollup, плюс общий итог:
SELECT status, SUM(total) AS revenue
FROM orders
GROUP BY ROLLUP (status);   -- = GROUPING SETS ((status), ())

-- Каждая комбинация status × user_id, плюс все подытоги и общий итог:
SELECT status, user_id, SUM(total)
FROM orders
GROUP BY CUBE (status, user_id);

ROLLUP (a, b) даёт n+1 множеств группировок; CUBE из n колонок даёт 2^n. Тянись за ROLLUP для иерархий (частые 90%) и за CUBE лишь когда тебе действительно нужна каждая кросс-комбинация — его вывод растёт экспоненциально с числом измерений.

Причина, по которой это бьёт старый паттерн UNION ALL, — число сканов. На таблице orders в 12М строк подсчёт по статусу, по пользователю и общего итога тремя SELECT, склеенными UNION ALL, — это три независимых Seq Scan: ~3 × 800мс ≈ 2.4с плюс стоимость перечитывания суммарно 36М строк. Форма GROUPING SETS в EXPLAIN ANALYZE выглядит как один Seq Scan (rows=12000000), питающий MixedAggregate (или GroupAggregate над отсортированным входом), который считает все три группировки за один проход — ~950мс, одно чтение 12М строк. Подвох CUBE — в выводе: CUBE по 4 измерениям — это 2⁴ = 16 множеств группировок, и на колонках высокой кардинальности каждое множество может быть миллионами строк — результат может затмить базовую таблицу. Postgres всё равно делает это за один скан входа, но состояние агрегации для 16 одновременных группировок может пробить work_mem и спиллить (HashAggregate ... Disk Usage), так что добавляй измерения в CUBE осознанно.

GROUPING(): этот NULL — подытог или настоящие данные?

Вот ловушка. У строки подытога NULL в колонках, по которым она не сгруппирована. Но твои данные могут также содержать настоящий NULL в status. Как отличить «все статусы» (подытог) от «заказа, чей status действительно NULL»? Функция GROUPING():

SELECT
  status,
  SUM(total) AS revenue,
  GROUPING(status) AS is_subtotal   -- 1 = строка подытога, 0 = настоящее значение status
FROM orders
GROUP BY ROLLUP (status);

GROUPING(col) возвращает 1, если col был свёрнут (агрегирован прочь) для этой строки — то есть это строка подытога/итога, — и 0, если col — настоящее сгруппированное значение (включая настоящий NULL). Используй её для разметки строк или чистого coalesce: CASE WHEN GROUPING(status) = 1 THEN 'All statuses' ELSE status END. Без GROUPING() настоящий NULL status и подытог неразличимы, и твой отчёт молча подписывает их одинаково.

Граничные случаи

GROUPING() может принимать несколько аргументов и возвращает битовую маску: GROUPING(a, b) даёт 2-битное целое, где каждый бит говорит, был ли свёрнут тот столбец. Для CUBE(a, b) GROUPING(a, b) возвращает 0 (оба настоящие), 1 (b свёрнут), 2 (a свёрнут) или 3 (общий итог). Эта битовая маска — чистый способ отсортировать или отфильтровать до конкретного уровня группировки в одном результате CUBE — например HAVING GROUPING(a, b) = 0 оставляет лишь полностью детальные строки. Да, GROUPING() можно класть в HAVING — это выражение в агрегатном контексте.

Викторина

Что GROUP BY ROLLUP (status) считает такого, чего не делает простой GROUP BY status?

Викторина

В результате ROLLUP у строки status = NULL. Как отличить строку подытога от настоящего NULL-статуса?

Закончи аналогию

Заполни пропуск: ROLLUP (a, b) разворачивается в иерархические множества группировок (a,b), (a), () — тогда как CUBE (a, b) разворачивается во _______ комбинации, включая (b) отдельно.

Вспомните перед уходом
  1. 01
    Какую проблему решают GROUPING SETS / ROLLUP / CUBE и что бы ты написал без них?
  2. 02
    Чем ROLLUP (a,b) отличается от CUBE (a,b) по множествам группировок, в которые разворачиваются?
  3. 03
    Зачем нужен GROUPING() и что он возвращает?
Итог

GROUPING SETS считает несколько группировок одним запросом и одним сканом таблицы, складывая их строки результата, — пустое множество () даёт общий итог. Это заменяет старый шаблон UNION ALL из одного запроса на группировку, который пересканировал таблицу и разъезжался. ROLLUP (a, b) — иерархическое сокращение для (a,b), (a), () — подытоги drill-down плюс общий итог, — а CUBE — исчерпывающее сокращение 2^n для каждой комбинации, его стоит применять умеренно, ведь он растёт экспоненциально. Тонкость — в NULL: строка подытога несёт NULL в свёрнутых колонках, неотличимый от настоящего NULL, поэтому GROUPING(col) (1 для свёрнутой строки/подытога, 0 для настоящего значения, битовая маска для нескольких колонок) — это способ корректно разметить, отсортировать или отфильтровать строки. Дальше применим инструменты условной агрегации, что ты выучил, для построения сводных таблиц — превращая длинные строки в широкие отчётные колонки через SUM(CASE …) и FILTER.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.