OLAP-куб: что это простыми словами и как собрать его в PostgreSQL

OLAP-куб хранит готовые итоги по всем сочетаниям разрезов: городам, товарам, месяцам.

Разбираем, чем OLAP отличается от OLTP и какие бывают кубы, а потом собираем куб в PostgreSQL через GROUP BY CUBE, ROLLUP и материализованное представление. Все запросы прогнаны в PostgreSQL 17, вывод приложен.

Статью написал:
Ваня Буявец, продюсер, основатель Checkroi
Ваня Буявец
Основатель Checkroi, продюсер, эксперт в выборе онлайн-курсов
Все 3267 статей автора Подписаться на Телеграм-канал
Одобрено экспертом:
Наташа Буявец, основатель Checkroi, эксперт по онлайн-курсам
Наташа Буявец
Основательница Checkroi, продюсер Youtube-каналов, эксперт по онлайн-курсам
Все 3921 экспертное мнение Подписаться на Телеграм-канал
Обложка: OLAP-куб: что это простыми словами и как собрать его в PostgreSQL

OLAP-куб — это заранее посчитанные итоги по всем сочетаниям разрезов данных: выручка по городам, по товарам, по месяцам и по любым их парам. Вместо того чтобы каждый раз гонять тяжёлый запрос по миллионам строк, аналитик берёт готовую цифру из нужной «ячейки» куба.

Отдельный OLAP-сервер для этого не обязателен. В PostgreSQL такой куб собирается одной конструкцией GROUP BY CUBE и сохраняется в материализованное представление. Ниже разберём, что такое OLAP простыми словами, и соберём куб на живом примере: все запросы прогнаны в PostgreSQL 17, вывод вставлен как есть. Если с языком SQL вы пока на «вы», начните с него.

Что такое 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 в свою таблицу), получится схема «снежинка». Она экономит место, но добавляет соединения в каждый запрос, поэтому под кубы чаще берут звезду.

Канал основателя Checkroi Вани Буявца3 800 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединиться

Какие бывают 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.

CheckroiCheckroiПодборка курсов по SQL430 курсов • 41 школаСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Как собрать 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), ()) вернёт пять строк: итог по каждому городу, по каждой категории и общий итог.

CheckroiCheckroiПодборка курсов по PostgreSQL157 курсов • 27 школСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Шаг 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.

Ваня БуявецКанал основателя Checkroi Вани БуявцаЗабирайте промпты и обучение по нейросетям в моём Телеграм-каналеБольше 3 800 человек уже применяют Claude Code, ChatGPT и другие нейросети в работе, учёбе, бизнесе и жизниПерейти в канал

Где ещё живут OLAP-кубы

PostgreSQL удобен, чтобы понять механику и закрыть отчётность небольшого проекта. В компаниях кубы встречаются в нескольких видах:

  • Классические OLAP-серверы вроде Microsoft Analysis Services: куб описывается моделью, а запросы пишутся на языке MDX.
  • Колоночные аналитические базы вроде ClickHouse: там GROUP BY … WITH CUBE и WITH ROLLUP работают по миллиардам строк без отдельной сборки куба.
  • BI-системы: модель данных в них играет роль куба, а срезы и детализация доступны мышкой. Данные в такую модель часто готовят через Power Query.

Принцип везде одинаков: меры, измерения и заранее посчитанные итоги. Кто освоил CUBE, ROLLUP и оконные функции SQL, тот быстро разберётся в любой из этих систем. Это базовый набор и для дата-инженера, который строит хранилище под кубы.

CheckroiCheckroiПодборка курсов по анализу данных591 курс • 60 школСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Где освоить SQL и аналитику системно

Чтобы собрать один куб, хватит этой инструкции. Чтобы проектировать хранилище, модель измерений и регламент обновлений, нужна системная практика с ревью. В подборке ниже курсы по SQL с ценами и сроками от разных школ, сравните их по программе и формату:

КурсШколаСтоимость со скидкойВ рассрочкуДлитель­ностьОбзор курса от Checkroi
Нейросети для рабочих задач
Перейти на сайт курса
SkillboxSkillbox31 290 ₽ - 1 месяцОбзор курса
SQL для анализа данных
Перейти на сайт курса
SkillboxSkillbox50 865 ₽6203 ₽/мес.2 месяцаОбзор курса
SQL-разработчик: тариф PRO
Перейти на сайт курса
Академия ЭдюсонЭдюсон89 900 ₽7491 ₽/мес.2 месяцаОбзор курса
SQL-разработчик: тариф Базовый
Перейти на сайт курса
Академия ЭдюсонЭдюсон79 900 ₽3329 ₽/мес.3 месяцаОбзор курса
Data-Driven подход в разработке продукта: SQL и Yandex DataLens
Перейти на сайт курса
ProductStarProductStar29 990 ₽2999 ₽/мес.1 месяцОбзор курса

Больше программ — в полном каталоге курсов по SQL

Часто задаваемые вопросы

Что такое OLAP-куб простыми словами?

Это таблица заранее посчитанных итогов по всем сочетаниям разрезов данных: например, выручка по каждому городу, каждой категории, каждому месяцу и по сети в целом. Отчёт берёт готовую цифру из куба и не пересчитывает миллионы строк.

Чем OLAP отличается от OLTP?

OLTP-база обслуживает приложение: тысячи коротких записей и чтений в секунду. OLAP-система работает с историей и отвечает на тяжёлые аналитические запросы вроде «выручка по регионам за год». Отчёты на рабочей OLTP-базе тормозят оформление заказов, поэтому данные для аналитики выгружают отдельно.

Можно ли сделать OLAP-куб в PostgreSQL без отдельного сервера?

Да. Итоги по всем сочетаниям даёт GROUP BY CUBE, а сохранить их можно в материализованное представление и обновлять командой REFRESH MATERIALIZED VIEW. Пример по шагам с выводом psql есть в статье. Такой куб относится к типу ROLAP.

Чем CUBE отличается от ROLLUP?

CUBE считает итоги по всем подмножествам измерений: для трёх измерений это восемь наборов группировки. ROLLUP идёт по иерархии и даёт только префиксы списка, например «месяц и город», «месяц» и общий итог. Для произвольного набора разрезов есть GROUPING SETS.

Как отличить строку итога от NULL в данных?

Функцией GROUPING(): она возвращает 1, если столбец в этой строке свёрнут в итог, и 0 для обычных значений, включая настоящий NULL. Подробнее о поведении NULL — в статье NULL в SQL.

Читайте Checkroi первым в Google
Добавьте Checkroi в избранные источники — и наши разборы курсов и обзоры школ будут показываться выше в вашей выдаче Google.
Оставить комментарий
0 комментариев
Форма комментария

Оставьте комментарий

Напишите, что думаете. Нам важно ваше мнение!