WITH ROLLUP, CUBE и TOTALS
Стандартный GROUP BY выдаёт строки только по конкретным комбинациям ключей. Но аналитические отчёты часто требуют подитоги: “продажи по странам и городам, плюс итог по стране, плюс общий итог”. Без модификаторов — это 3 отдельных запроса с UNION ALL.
ClickHouse поддерживает 3 модификатора GROUP BY:
- WITH ROLLUP — иерархические подитоги (справа налево)
- WITH CUBE — все возможные комбинации (2^n)
- WITH TOTALS — одна строка общего итога
WITH ROLLUP: иерархические подитоги
ROLLUP удаляет столбцы группировки справа налево, создавая подитоги на каждом уровне:
SELECT country, city, sum(revenue) AS total
FROM sales
GROUP BY country, city WITH ROLLUP
ORDER BY country, city;
| country | city | total |
|---|---|---|
| Germany | Berlin | 5000 |
| Germany | Munich | 3000 |
| Germany | 8000 | |
| Russia | Moscow | 7000 |
| Russia | SPb | 4000 |
| Russia | 11000 | |
| 19000 |
Для GROUP BY a, b, c WITH ROLLUP создаются уровни:
(a, b, c)— детальные строки(a, b)— подитог без c(a)— подитог без b, c()— общий итог
Итого: n + 1 уровень для n столбцов.
WITH CUBE: все комбинации
CUBE создаёт подитоги для всех возможных комбинаций столбцов GROUP BY:
SELECT year, quarter, sum(revenue) AS total
FROM sales
GROUP BY year, quarter WITH CUBE;
Для GROUP BY a, b WITH CUBE создаются комбинации:
(a, b)— детальные строки(a)— подитог без b(b)— подитог без a()— общий итог
Для n столбцов: 2^n комбинаций.
| year | quarter | total |
|---|---|---|
| 2025 | Q1 | 10000 |
| 2025 | Q2 | 12000 |
| 2026 | Q1 | 15000 |
| 2025 | 22000 | |
| 2026 | 15000 | |
| Q1 | 25000 | |
| Q2 | 12000 | |
| 37000 |
ROLLUP подходит для иерархий (страна, город, район). CUBE — для перекрёстных отчётов (год, квартал, регион), где важны подитоги по каждому измерению независимо.
WITH TOTALS: одна строка итога
WITH TOTALS добавляет одну дополнительную строку с общим итогом. В отличие от ROLLUP и CUBE, не создаёт промежуточных подитогов:
SELECT country, sum(revenue) AS total
FROM sales
GROUP BY country WITH TOTALS;
| country | total |
|---|---|
| Germany | 8000 |
| Russia | 11000 |
| 19000 |
WITH TOTALS особенно полезен с HAVING: итог вычисляется до фильтрации HAVING, показывая полную картину:
SELECT country, sum(revenue) AS total
FROM sales
GROUP BY country WITH TOTALS
HAVING total > 10000;
| country | total |
|---|---|
| Russia | 11000 |
| 19000 |
GROUPING(): дизамбигуация подитогов
Главная проблема ROLLUP и CUBE: подитоги имеют default-значения (пустая строка, 0) в агрегированных столбцах. Как отличить подитог от реальной строки с пустым значением?
-- Проблема: country='' -- это подитог или реальная страна с пустым названием?
SELECT country, city, sum(revenue) AS total
FROM sales
GROUP BY country, city WITH ROLLUP;
Функция GROUPING() решает эту проблему. Она возвращает битовую маску, показывающую какие столбцы были агрегированы:
SELECT
country,
city,
sum(revenue) AS total,
GROUPING(country) AS g_country,
GROUPING(city) AS g_city
FROM sales
GROUP BY country, city WITH ROLLUP
ORDER BY g_country, g_city, country, city;
| country | city | total | g_country | g_city |
|---|---|---|---|---|
| Germany | Berlin | 5000 | 0 | 0 |
| Germany | Munich | 3000 | 0 | 0 |
| Russia | Moscow | 7000 | 0 | 0 |
| Russia | SPb | 4000 | 0 | 0 |
| Germany | 8000 | 0 | 1 | |
| Russia | 11000 | 0 | 1 | |
| 19000 | 1 | 1 |
GROUPING(col) = 0— столбец участвует в группировке (реальное значение)GROUPING(col) = 1— столбец агрегирован (значение — подитог)
-- Множественный GROUPING() с битовой маской:
SELECT
country, city,
sum(revenue) AS total,
GROUPING(country, city) AS grp
FROM sales
GROUP BY country, city WITH ROLLUP;
-- grp=0: обычная строка (country + city)
-- grp=1: подитог по country (city агрегирован)
-- grp=3: общий итог (оба агрегированы)
Без GROUPING() подитоги неотличимы от строк с реальными пустыми значениями. Всегда используйте GROUPING() если данные могут содержать пустые строки или нули в столбцах GROUP BY.
Практический пример: отчёт о продажах
SELECT
CASE WHEN GROUPING(region) = 1 THEN 'ВСЕ РЕГИОНЫ' ELSE region END AS region,
CASE WHEN GROUPING(category) = 1 THEN 'ВСЕ КАТЕГОРИИ' ELSE category END AS category,
sum(revenue) AS revenue,
count() AS orders,
avg(revenue) AS avg_order
FROM sales
WHERE sale_date >= '2025-01-01'
GROUP BY region, category WITH CUBE
ORDER BY GROUPING(region, category), region, category;
Результат — полная перекрёстная таблица с читаемыми подписями подитогов вместо пустых строк.
Сравнение модификаторов
| Модификатор | Дополнительные строки | Сценарий |
|---|---|---|
| WITH ROLLUP | n + 1 уровень (иерархия) | Иерархические отчёты: страна, город, район |
| WITH CUBE | 2^n комбинаций | Перекрёстные отчёты: год, квартал, категория |
| WITH TOTALS | 1 строка (общий итог) | Простой итог, особенно полезен с HAVING |
Ключевые выводы
- WITH ROLLUP — иерархические подитоги, столбцы удаляются справа налево. Идеален для drill-down отчётов.
- WITH CUBE — все 2^n комбинаций. Для перекрёстных отчётов с независимыми измерениями.
- WITH TOTALS — одна строка общего итога. Показывает total до HAVING-фильтрации.
- GROUPING() — обязательна для дизамбигуации: отличает подитоги от реальных пустых значений.
- Без GROUPING() нельзя надёжно отличить подитог от строки с
country = ''.