OLAP-куб — это заранее посчитанные итоги по всем сочетаниям разрезов данных: выручка по городам, по товарам, по месяцам и по любым их парам. Вместо того чтобы каждый раз гонять тяжёлый запрос по миллионам строк, аналитик берёт готовую цифру из нужной «ячейки» куба.
Отдельный OLAP-сервер для этого не обязателен. В PostgreSQL такой куб собирается одной конструкцией GROUP BY CUBE и сохраняется в материализованное представление. Ниже разберём, что такое OLAP простыми словами, и соберём куб на живом примере: все запросы прогнаны в PostgreSQL 17, вывод вставлен как есть. Если с языком SQL вы пока на «вы», начните с него.
SkillboxНейросети для рабочих задач31 290 ₽1 месяц
ЭдюсонSQL-разработчик: тариф PRO89 900 ₽2 месяца
ProductStarData-Driven подход в разработке продукта: SQL и Yandex DataLens29 990 ₽1 месяц
SkyproSQL с нуля134 640 ₽12 месяцев
SF EducationОбработка и анализ данных в SQL11 550 ₽1 месяц
НетологияSQL и получение данных33 900 ₽9 недель
Что такое OLAP-куб простыми словами

Возьмите сводную таблицу Excel: в строках города, в столбцах месяцы, в ячейках сумма продаж. Это маленький двумерный куб. Добавьте третий разрез, например категорию товара, и получится настоящий куб данных: у него три оси, а в каждой ячейке лежит число.
У любого куба две составные части:
- Меры (measures): то, что считаем: выручка, количество заказов, средний чек.
- Измерения (dimensions): то, по чему режем: время, география, товар, канал продаж. Внутри измерения бывает иерархия: год → квартал → месяц.
В документации Microsoft к Analysis Services куб описан так же: простой объект куба состоит из основных сведений, измерений и групп мер.
Главное свойство куба: в нём уже есть итоги по каждому сочетанию измерений, включая «все города» и «все товары». Поэтому отчёт «выручка Казани по выпечке за октябрь» и отчёт «выручка всей сети за год» отвечают одинаково быстро. Ровно этим занимается аналитик данных, когда крутит дашборд: он просит у куба разные срезы и не пишет новый запрос под каждый вопрос.
OLAP и OLTP: в чём разница
Аббревиатура OLAP расшифровывается как online analytical processing, «оперативная аналитическая обработка данных». Противоположность OLAP называется OLTP, online transaction processing: это базы, в которые приложение пишет заказы, платежи и регистрации.
| OLTP | OLAP | |
|---|---|---|
| Типичный запрос | записать один заказ, найти клиента по id | сумма по миллиону заказов в разрезе месяцев и регионов |
| Кто работает | приложение, тысячи коротких операций в секунду | аналитики и BI-отчёты, редкие тяжёлые запросы |
| Данные | текущие, постоянно меняются | история, обновляются пачками раз в час или раз в сутки |
| Структура | нормализованные таблицы | таблица фактов и справочники-измерения, готовые агрегаты |
Строить отчёты прямо на рабочей OLTP-базе плохо: тяжёлый аналитический запрос тормозит оформление заказов. Поэтому данные выгружают в отдельное хранилище, а собирает этот конвейер ETL-разработчик. Уже поверх хранилища строятся кубы и дашборды.
Звезда: как устроены данные под кубом
В хранилище данные для куба обычно раскладывают по схеме «звезда». В центре лежит таблица фактов: одна строка на событие (продажу, визит, звонок), в ней числа и ключи. Вокруг неё стоят таблицы измерений со справочной информацией.
fact_sales: дата, id магазина, id товара, сумма, количество.dim_date: дата, месяц, квартал, год, признак выходного.dim_store: магазин, город, регион.dim_product: товар, категория, бренд.
Иерархии измерений живут как раз в справочниках: «магазин → город → регион» укладывается в три столбца одной таблицы dim_store. Куб собирают запросом, который соединяет факты со справочниками через JOIN и группирует по нужным уровням. В нашем примере ниже справочники для простоты уже «вклеены» в таблицу фактов: город и категория лежат прямо в строке продажи.
Если измерения ещё и нормализовать (вынести регион из dim_store в свою таблицу), получится схема «снежинка». Она экономит место, но добавляет соединения в каждый запрос, поэтому под кубы чаще берут звезду.
Какие бывают OLAP-кубы
По способу хранения кубы делят на три типа. Именно эти три режима называет Microsoft: многомерный режим Analysis Services работает с режимами хранения MOLAP, ROLAP и HOLAP.
- MOLAP (multidimensional): куб хранится в собственном формате OLAP-сервера, все агрегаты посчитаны заранее. Запросы самые быстрые, зато пересборка долгая.
- ROLAP (relational): данные лежат в обычных таблицах реляционной базы, итоги считаются SQL-запросами на лету или хранятся в агрегатных таблицах. Наш пример на PostgreSQL ниже относится к ROLAP.
- HOLAP (hybrid): детальные строки остаются в реляционной базе, а готовые агрегаты лежат в многомерном хранилище.
Для начинающего разница важна в одном месте: куб не обязан быть отдельным сервером. Его можно собрать в той же базе, где лежат данные, и BI-аналитик часто так и делает на старте проекта.
Пять операций с кубом

У работы с кубом есть свой словарь. Всё это делается и в BI-интерфейсе мышкой, и обычным SQL.
| Операция | Что делает | Пример вопроса |
|---|---|---|
| Срез (slice) | фиксирует одно значение измерения | «что было только в октябре» |
| Кубик (dice) | фиксирует несколько измерений сразу | «октябрь, Казань, кофе» |
| Детализация (drill-down) | спускается по иерархии вниз | «из года в месяцы» |
| Свёртка (roll-up) | поднимается по иерархии вверх | «из городов в итог по сети» |
| Поворот (pivot) | меняет местами строки и столбцы | «месяцы в столбцы, города в строки» |
Поворот в SQL делается условной агрегацией, подробно техника разобрана в статье про PIVOT в SQL. Остальные четыре операции дают конструкции CUBE, ROLLUP и обычный WHERE.
Как собрать OLAP-куб в PostgreSQL: пошаговая инструкция
Возьмём сеть кофеен: два города, две категории, два месяца. Восемь строк хватит, чтобы увидеть всю механику, а на миллионе строк запросы те же.
Шаг 1: Таблица фактов
Создайте таблицу в pgAdmin, в psql или в DBeaver и заполните её:
CREATE TABLE sales (
month date,
city text,
category text,
revenue numeric
);
INSERT INTO sales VALUES
('2026-09-01', 'Москва', 'Кофе', 420000),
('2026-09-01', 'Москва', 'Выпечка', 180000),
('2026-09-01', 'Казань', 'Кофе', 210000),
('2026-09-01', 'Казань', 'Выпечка', 95000),
('2026-10-01', 'Москва', 'Кофе', 460000),
('2026-10-01', 'Москва', 'Выпечка', 175000),
('2026-10-01', 'Казань', 'Кофе', 240000),
('2026-10-01', 'Казань', 'Выпечка', 110000);
Здесь revenue служит мерой, а month, city и category измерениями.
Шаг 2: Итоги по всем сочетаниям через GROUP BY CUBE
Обычный GROUP BY city, category даст четыре строки. CUBE добавит к ним промежуточные итоги и общий итог:
SELECT city, category, sum(revenue) AS revenue
FROM sales
GROUP BY CUBE (city, category)
ORDER BY city, category;
city | category | revenue
--------+----------+---------
Казань | Выпечка | 205000
Казань | Кофе | 450000
Казань | | 655000
Москва | Выпечка | 355000
Москва | Кофе | 880000
Москва | | 1235000
| Выпечка | 560000
| Кофе | 1330000
| | 1890000
(9 rows)
Пустая ячейка означает «по всем значениям». Строка «Казань, пусто, 655 000» показывает всю Казань, нижняя строка показывает всю сеть.
По документации PostgreSQL, CUBE представляет заданный список и все его возможные подмножества. Для трёх измерений это восемь наборов группировки: (a, b, c), (a, b), (a, c), (a), (b, c), (b), (c) и пустой набор.
Шаг 3: Подписи итогов через GROUPING
Пустые ячейки неудобны для отчёта. Функция GROUPING() говорит, какой столбец в текущей строке свёрнут в итог: по документации она выдаёт битовую маску, показывающую, какие выражения GROUP BY не вошли в текущий набор группирования. Подставим подписи через оператор CASE:
SELECT
CASE WHEN GROUPING(city) = 1 THEN 'Все города' ELSE city END AS city,
CASE WHEN GROUPING(category) = 1 THEN 'Все категории' ELSE category END AS category,
sum(revenue) AS revenue
FROM sales
GROUP BY CUBE (city, category)
ORDER BY GROUPING(city), city, GROUPING(category), category;
city | category | revenue
------------+---------------+---------
Казань | Выпечка | 205000
Казань | Кофе | 450000
Казань | Все категории | 655000
Москва | Выпечка | 355000
Москва | Кофе | 880000
Москва | Все категории | 1235000
Все города | Выпечка | 560000
Все города | Кофе | 1330000
Все города | Все категории | 1890000
(9 rows)
Шаг 4: Иерархия через ROLLUP
Когда измерения вложены друг в друга (месяц → город), все сочетания не нужны. ROLLUP по документации представляет заданный список выражений и всех префиксов списка, включая пустой список, и подходит для иерархий:
SELECT to_char(month, 'YYYY-MM') AS month, city, sum(revenue) AS revenue
FROM sales
GROUP BY ROLLUP (month, city)
ORDER BY month, city;
month | city | revenue
---------+--------+---------
2026-09 | Казань | 305000
2026-09 | Москва | 600000
2026-09 | | 905000
2026-10 | Казань | 350000
2026-10 | Москва | 635000
2026-10 | | 985000
| | 1890000
(7 rows)
Это и есть свёртка: города → месяц → вся сеть. Месяц здесь получен из даты функцией to_char; другие способы достать год и месяц разобраны в статье про EXTRACT в PostgreSQL. Строки «Казань за всё время» здесь нет, потому что город стоит вторым в иерархии.
Если нужен произвольный набор разрезов, перечислите его явно через GROUPING SETS. Запрос GROUP BY GROUPING SETS ((city), (category), ()) вернёт пять строк: итог по каждому городу, по каждой категории и общий итог.
Шаг 5: Сохраняем куб в материализованное представление
Пока что итоги считаются при каждом запросе. Чтобы получился настоящий куб с готовыми ячейками, сохраните результат:
CREATE MATERIALIZED VIEW sales_cube AS
SELECT month, city, category,
GROUPING(month, city, category) AS grp,
sum(revenue) AS revenue
FROM sales
GROUP BY CUBE (month, city, category);
SELECT count(*) AS cells FROM sales_cube;
cells
-------
27
(1 row)
Готовые ячейки удобно отдавать в BI-отчёт или выгружать в сводную таблицу Excel. Откуда 27: у каждого из трёх измерений два значения плюс «все», то есть 3 × 3 × 3. Столбец grp хранит маску GROUPING: 0 у детальных строк, 7 у общего итога, где свёрнуты все три измерения.
Шаг 6: Берём срез из куба
Теперь срез делается простым WHERE по готовым ячейкам без пересчёта:
SELECT city, category, revenue
FROM sales_cube
WHERE month = '2026-10-01' AND grp = 0
ORDER BY city, category;
city | category | revenue
--------+----------+---------
Казань | Выпечка | 110000
Казань | Кофе | 240000
Москва | Выпечка | 175000
Москва | Кофе | 460000
(4 rows)
Шаг 7: Обновляем куб после новых данных
Материализованное представление само не обновляется. Добавим продажу на 15 000 ₽ и посмотрим на общий итог (grp = 7) до и после обновления:
INSERT INTO sales VALUES ('2026-10-01', 'Казань', 'Кофе', 15000);
SELECT revenue FROM sales_cube WHERE grp = 7; -- 1890000, куб ещё старый
REFRESH MATERIALIZED VIEW sales_cube;
SELECT revenue FROM sales_cube WHERE grp = 7; -- 1905000
Команда REFRESH, как сказано в документации, полностью заменяет содержимое материализованного представления. Обычное обновление может мешать другим сеансам читать куб. Если отчёты должны работать без пауз, используйте REFRESH MATERIALIZED VIEW CONCURRENTLY, но для него нужен уникальный индекс:
CREATE UNIQUE INDEX sales_cube_key
ON sales_cube (grp, month, city, category) NULLS NOT DISTINCT;
REFRESH MATERIALIZED VIEW CONCURRENTLY sales_cube;
Оговорка NULLS NOT DISTINCT нужна из-за строк итогов, где измерения равны NULL. По умолчанию NULL для уникального индекса считаются различными, и индекс пропустил бы дубль итоговой строки без ошибки. Обновление пройдёт и без оговорки, но тогда уникальность итогов никто не проверяет. Пересборку куба обычно ставят в расписание: cron, pg_cron или шаг в Airflow после загрузки данных.
Частые ошибки и что с ними делать

Настоящий NULL в данных путается с итогом
Симптом: в отчёте две строки с пустым городом и непонятно, какая из них итог. Так бывает, когда у части продаж город не заполнен. Мы добавили продажу на 30 000 ₽ без города, и ROLLUP (city) вернул две пустые строки: на 30 000 и на 1 935 000.
Решение: различайте строки по GROUPING(): пустое значение здесь ничего не говорит. Почему NULL ведёт себя не как пустая строка, разобрано в статье NULL в SQL.
SELECT city, GROUPING(city) AS is_total, sum(revenue) AS revenue
FROM sales
GROUP BY ROLLUP (city)
ORDER BY city, is_total;
city | is_total | revenue
--------+----------+---------
Казань | 0 | 670000
Москва | 0 | 1235000
| 0 | 30000
| 1 | 1935000
(4 rows)
Куб разрастается
Симптом: материализованное представление весит больше исходной таблицы и обновляется часами. Число наборов группировки в CUBE удваивается с каждым измерением: три измерения дают восемь наборов, а шесть измерений уже 64.
Решение: в CUBE кладите только те разрезы, которые открывают в отчётах. Для иерархий берите ROLLUP, а редкие сочетания перечисляйте в GROUPING SETS.
Неаддитивная мера суммируется в итог
Симптом: «число уникальных клиентов» по сети получается больше реального. Если сложить уникальных клиентов Москвы и Казани, человек, покупавший в обоих городах, посчитается дважды.
Решение: считайте такие меры прямо в запросе куба через count(DISTINCT customer_id), тогда каждая ячейка получит правильное значение. Готовые ячейки с такой мерой между собой не складывайте. Как работает COUNT с DISTINCT и по группам, разобрано отдельно.
В ClickHouse итоги пустые, но не NULL
Симптом: запрос с WITH CUBE, перенесённый из PostgreSQL в ClickHouse, не находит итоговые строки по IS NULL. В документации ClickHouse сказано, что в строках промежуточных итогов значения всех «сгруппированных» ключевых выражений устанавливаются в 0 или пустую строку.
Решение: включите там же описанную настройку group_by_use_nulls или отбирайте итоги функцией GROUPING.
Где ещё живут OLAP-кубы
PostgreSQL удобен, чтобы понять механику и закрыть отчётность небольшого проекта. В компаниях кубы встречаются в нескольких видах:
- Классические OLAP-серверы вроде Microsoft Analysis Services: куб описывается моделью, а запросы пишутся на языке MDX.
- Колоночные аналитические базы вроде ClickHouse: там
GROUP BY … WITH CUBEиWITH ROLLUPработают по миллиардам строк без отдельной сборки куба. - BI-системы: модель данных в них играет роль куба, а срезы и детализация доступны мышкой. Данные в такую модель часто готовят через Power Query.
Принцип везде одинаков: меры, измерения и заранее посчитанные итоги. Кто освоил CUBE, ROLLUP и оконные функции SQL, тот быстро разберётся в любой из этих систем. Это базовый набор и для дата-инженера, который строит хранилище под кубы.
Где освоить SQL и аналитику системно
Чтобы собрать один куб, хватит этой инструкции. Чтобы проектировать хранилище, модель измерений и регламент обновлений, нужна системная практика с ревью. В подборке ниже курсы по SQL с ценами и сроками от разных школ, сравните их по программе и формату:
| Курс | Школа | Стоимость со скидкой | В рассрочку | Длительность | Обзор курса от Checkroi |
|---|---|---|---|---|---|
| Нейросети для рабочих задач Перейти на сайт курса | 31 290 ₽ | - | 1 месяц | Обзор курса | |
| SQL для анализа данных Перейти на сайт курса | 50 865 ₽ | 6203 ₽/мес. | 2 месяца | Обзор курса | |
| SQL-разработчик: тариф PRO Перейти на сайт курса | 89 900 ₽ | 7491 ₽/мес. | 2 месяца | Обзор курса | |
| SQL-разработчик: тариф Базовый Перейти на сайт курса | 79 900 ₽ | 3329 ₽/мес. | 3 месяца | Обзор курса | |
| Data-Driven подход в разработке продукта: SQL и Yandex DataLens Перейти на сайт курса | 29 990 ₽ | 2999 ₽/мес. | 1 месяц | Обзор курса |
Больше программ — в полном каталоге курсов по SQL




