Раздел 42 · Аналитика данных с нуля

ROLLUP, CUBE и GROUPING SETS: итоги и подытоги одним запросом

middle~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Утро после разбора у Ильвы. Лада вернула Киму его письмо с сырыми 53,3%, исчёрканное красным, и Ким весь вечер молчал. До решения «Монетки» шесть недель, и Брана сейчас подводить нельзя: его студия один из двух кейсов в портфолио для тендера.

Блок F позади, и мы возвращаемся к Брану и его магазину. Выручку по месяцам и платформам ты уже умеешь считать: GROUP BY по двум столбцам даёт 9 строк, по три платформы в каждом из трёх месяцев. Новое в просьбе Брана это итоги. Отчёт, в котором под деталями стоят суммы, называют отчётом с подытогами, и в SQL для него есть отдельные инструменты.

Выручку везде считаем по чистым покупкам из первого босса: оплачены, один раз на receipt_id, без тестовых аккаунтов студии. В примерах ниже этот CTE называется clean:

WITH clean AS (
  SELECT *
  FROM purchases
  WHERE status = 'paid'
    AND id IN (SELECT min(id) FROM purchases GROUP BY receipt_id)
    AND player_id NOT IN (SELECT id FROM players WHERE email LIKE '%@studio.dev')
)

Итоги руками: три запроса и клей

Что ты сделал бы с тем, что уже знаешь? Три отдельных запроса, по одному на уровень, и склейка через UNION ALL из урока про операции над множествами:

SELECT date_trunc('month', created_at)::date AS month, platform, sum(amount_rub) AS revenue
FROM clean WHERE created_at < '2026-04-01'
GROUP BY 1, 2
UNION ALL
SELECT date_trunc('month', created_at)::date, NULL, sum(amount_rub)
FROM clean WHERE created_at < '2026-04-01'
GROUP BY 1
UNION ALL
SELECT NULL, NULL, sum(amount_rub)
FROM clean WHERE created_at < '2026-04-01';

Работает, но посмотри, сколько тут места для ошибки. Фильтр по датам повторён трижды: поправишь в одной части и забудешь в другой, и итог перестанет сходиться с деталями. Пустые столбцы итогов заполнены NULL вручную. А база трижды читает одни и те же покупки.

ROLLUP: итоги лесенкой

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

SELECT date_trunc('month', created_at)::date AS month,
       platform,
       sum(amount_rub) AS revenue
FROM clean
WHERE created_at < '2026-04-01'
GROUP BY ROLLUP (date_trunc('month', created_at)::date, platform)
ORDER BY month, platform;
   month    | platform |  revenue
------------+----------+-----------
 2026-01-01 | android  |  32260.00
 2026-01-01 | ios      |  15749.00
 2026-01-01 | pc       |   7378.00
 2026-01-01 | NULL     |  55387.00
 2026-02-01 | android  |  42746.00
 2026-02-01 | ios      |  14854.00
 2026-02-01 | pc       |   5280.00
 2026-02-01 | NULL     |  62880.00
 2026-03-01 | android  |  80198.00
 2026-03-01 | ios      |  35079.00
 2026-03-01 | pc       |  47244.00
 2026-03-01 | NULL     | 162521.00
 NULL       | NULL     | 280788.00

Тринадцать строк: девять деталей, три итога месяцев (там platform равна NULL) и общий итог, где NULL в обоих столбцах. Один запрос, одна чистка, одно чтение таблицы.

ROLLUP (a, b) сворачивает столбцы справа налево и считает три уровня: (a, b), (a) и (). Пустые скобки означают «без группировки», то есть итог по всем строкам. Поэтому порядок внутри скобок важен: ROLLUP (platform, month) дал бы итоги по платформам за квартал, а итогов месяцев в нём не было бы.

урок из курса

Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.