Утро после разбора у Ильвы. Лада вернула Киму его письмо с сырыми 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 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.