Асинхронные материализованные представления в StarRocks

Асинхронные материализованные представления в StarRocks

Типичное хранилище со временем обрастает зоопарком витрин. Десятки скриптов на cron пересчитывают агрегаты, гоняют одни и те же джойны по кругу и жгут процессорное время. Часть этой работы можно убрать внутрь самой СУБД. В StarRocks для этого есть асинхронные материализованные представления, которые обновляются по расписанию и инкрементально, по одной партиции. В этой статье заменим внешний ETL на материализованное представление, разберем отличие от синхронных представлений ClickHouse и покажем работу на стенде с базой shop.

Материал продолжает серию про StarRocks и опирается на те же таблицы orders, order_items, products и customers. Грамотное проектирование слоев данных экономит терабайты места и часы процессорного времени, поэтому по ходу отмечаем, где именно возникает экономия.

 

Проблема зоопарка витрин и лишних вычислений

Классическая схема выглядит так. Сырые данные лежат в слое ODS. Поверх них внешний ETL считает витрины: выручку по регионам, топ товаров, активность клиентов. Каждая витрина это отдельный скрипт, отдельное расписание и отдельная точка отказа. Когда витрин становится много, инженер тратит больше времени на поддержку конвейеров, чем на аналитику.

Проблема зоопарка витрин c ODS и ETL

Вторая беда это избыточные вычисления. Если исходные данные за прошлые месяцы не менялись, пересчитывать их каждую ночь бессмысленно. Но простой ETL этого не знает и гоняет полный пересчет снова и снова. На объемах в сотни миллионов строк это выливается в часы работы кластера и лишний счет за ресурсы. Материализованное представление с партиционированием решает обе проблемы: логика витрины живет внутри базы одним объектом, а обновляется только то, что реально изменилось.

Полезно держать в голове классические слои хранилища. Слой ODS хранит сырые данные как есть. Слой витрин отдает пользователю готовые агрегаты. Раньше между ними стоял внешний оркестратор с расписаниями и зависимостями. Материализованное представление стягивает этот промежуточный слой внутрь СУБД, и оркестрация становится частью самой базы, а не отдельной системой, которую надо сопровождать.

как меняется подход к созданию витрин с материализованными представлениями StarRocks

 

Синхронные представления ClickHouse против асинхронных StarRocks

Материализованные представления есть и в ClickHouse, но работают они иначе. Представление ClickHouse это по сути триггер на вставку. Когда в исходную таблицу приходит новый блок строк, представление тут же обрабатывает именно этот блок и дописывает результат в целевую таблицу. Подход быстрый для потока, но у него есть особенности. Представление видит только новые вставки и не пересчитывает исторические данные автоматически. Тяжелая логика в представлении замедляет саму вставку, потому что выполняется синхронно.

 

StarRocks идет от расписания. Асинхронное представление отвязано от вставок. Оно обновляется по таймеру, например раз в десять минут, и умеет считать не только новые строки, но и пересобирать историю. Если базовая таблица партиционирована по дате, StarRocks обновляет только те партиции представления, чьи исходные данные изменились. Вставка при этом не замедляется, потому что обновление идет отдельным фоновым процессом.

Сведем различия в таблицу.

Аспект StarRocks, асинхронное MV ClickHouse, материализованное представление
Модель обновления по расписанию, фоновым процессом триггер на каждую вставку
Влияние на вставку вставка не замедляется тяжелая логика тормозит вставку
Историческая перезагрузка пересобирает историю, в том числе по партициям видит только новые блоки
Инкрементальность обновление по изменившимся партициям по вставленным блокам
Джойны нескольких таблиц поддерживаются в определении MV ограничены, обычно по одной таблице

Из таблицы виден и компромисс. Синхронное представление ClickHouse дает минимальную задержку, результат готов сразу после вставки. Асинхронное представление StarRocks обновляется по таймеру, поэтому между изменением источника и обновлением витрины проходит интервал расписания. Если нужна свежесть до секунды, интервал ставят коротким или запускают обновление вручную. Если витрина обновляется раз в час и этого достаточно, асинхронный подход экономит ресурсы, потому что не дергает пересчет на каждую вставку. По сути выбор между подходами это выбор между свежестью и стоимостью, и StarRocks дает крутить этот баланс расписанием.

 

Построение DWH на ClickHouse

Код курса
CLICH
Ближайшая дата курса
12 октября, 2026
Продолжительность
24 ак.часов
Стоимость обучения
76 800

Практика: заменяем ETL материализованным представлением

Все команды ниже собраны в файл ~/article04/mv_demo.sql и выполняются через MySQL-клиент. Подключаемся к StarRocks и выбираем базу.

# протестировано для StarRocks 3.5.0
mysql -h 127.0.0.1 -P 9030 -u root
USE shop;

 

Слой ODS с партиционированной таблицей

Сначала готовим слой сырых данных. Заводим таблицу заказов, партиционированную по месяцу. Партиционирование это ключ к инкрементальному обновлению: позже StarRocks будет пересчитывать витрину по одной партиции, а не целиком. Выражение date_trunc создает партиции автоматически при вставке.

-- протестировано для StarRocks 3.5.0
CREATE TABLE ods_orders (
    order_id    BIGINT,
    customer_id BIGINT,
    order_date  DATE,
    status      VARCHAR(16)
)
DUPLICATE KEY(order_id)
PARTITION BY date_trunc('month', order_date)
DISTRIBUTED BY HASH(order_id) BUCKETS 48;

-- наполняем ODS из уже загруженной таблицы orders
INSERT INTO ods_orders
SELECT order_id, customer_id, order_date, status FROM orders;

-- проверяем, что партиции по месяцам создались сами
SHOW PARTITIONS FROM ods_orders;

Проверка создания партиций при загрузке данных в StarRocks

 

Создаем асинхронное материализованное представление

Теперь заменяем внешний ETL одним объектом. Представление считает выручку по дате, региону и категории, внутри джойн четырех таблиц и агрегация. Ключевые части определения это REFRESH ASYNC EVERY, которое задает расписание, и PARTITION BY, которое выравнивает партиции витрины с партициями ODS.

-- протестировано для StarRocks 3.5.0
CREATE MATERIALIZED VIEW mv_daily_revenue
PARTITION BY date_trunc('month', order_date)
DISTRIBUTED BY HASH(region_id) BUCKETS 8
REFRESH ASYNC EVERY (INTERVAL 10 MINUTE)
AS
SELECT
    o.order_date,
    c.region_id,
    p.category_id,
    SUM(oi.quantity * p.price) AS revenue,
    COUNT(DISTINCT o.order_id) AS orders_cnt
FROM order_items oi
JOIN ods_orders o ON oi.order_id = o.order_id
JOIN products   p ON oi.product_id = p.product_id
JOIN customers  c ON o.customer_id = c.customer_id
GROUP BY o.order_date, c.region_id, p.category_id;

Сразу после создания StarRocks запускает первое полное обновление. На больших таблицах оно займет время, потому что считается весь джойн. Дальше обновления пойдут по расписанию и только по изменившимся партициям, это и есть уход от полного пересчета. Обратите внимание, что определение витрины это обычный SELECT. Вся бизнес-логика агрегации остается читаемой в одном месте, и ее легко поменять, пересоздав представление, без правки внешних скриптов и расписаний в стороннем оркестраторе.

 

Смотрим состояние и историю обновлений

Состояние представления и результат последнего обновления видно в системном представлении. Поле last_refresh_state показывает SUCCESS, RUNNING или FAILED.

SELECT table_name, is_active, last_refresh_state, last_refresh_finished_time
FROM information_schema.materialized_views
WHERE table_schema = 'shop';

-- компактный статус
SHOW MATERIALIZED VIEWS FROM shop;

как обновить статус материализованного обновление в StarRocks

Историю всех запусков обновления показывает системное представление задач. Каждая строка это один refresh с его статусом и временем. Именно сюда смотрят, когда витрина отстала или обновление упало.

SELECT task_name, state, create_time, finish_time, error_message
FROM information_schema.task_runs
ORDER BY create_time DESC
LIMIT 10;

 

Читаем витрину как обычную таблицу

Запрос к представлению идет по готовым агрегатам, без повторного джойна четырех таблиц. Это и есть выигрыш: тяжелый расчет сделан один раз при обновлении, а BI-запросы бьют по компактной витрине.

SELECT region_id, category_id, SUM(revenue) AS revenue
FROM mv_daily_revenue
WHERE order_date >= '2025-04-01'
GROUP BY region_id, category_id
ORDER BY revenue DESC
LIMIT 20;

Запрос к представлению идет по готовым агрегатам, без повторного джойна четырех таблиц

 

Прозрачное ускорение запросов через переписывание

У асинхронных представлений StarRocks есть свойство, которого нет у обычного ETL. Оптимизатор умеет сам переписать запрос, написанный к исходным таблицам, так, чтобы он взял готовые данные из представления. Аналитику не нужно знать имя витрины и менять свои запросы. Он пишет привычный джойн с агрегацией, а StarRocks подставляет подходящее представление прозрачно.

-- запрос написан к исходным таблицам, но может выполниться по mv_daily_revenue
EXPLAIN
SELECT c.region_id, p.category_id, SUM(oi.quantity * p.price) AS revenue
FROM order_items oi
JOIN ods_orders o ON oi.order_id = o.order_id
JOIN products   p ON oi.product_id = p.product_id
JOIN customers  c ON o.customer_id = c.customer_id
WHERE o.order_date >= '2025-04-01'
GROUP BY c.region_id, p.category_id;

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

+---------------------------------------------------------------------------------+
| Explain String                                                                  |
+---------------------------------------------------------------------------------+
| PLAN FRAGMENT 0                                                                 |
|  OUTPUT EXPRS:15: region_id | 11: category_id | 18: sum                         |
|   PARTITION: UNPARTITIONED                                                      |
|                                                                                 |
|   RESULT SINK                                                                   |
|                                                                                 |
|   6:EXCHANGE                                                                    |
|                                                                                 |
| PLAN FRAGMENT 1                                                                 |
|  OUTPUT EXPRS:                                                                  |
|   PARTITION: HASH_PARTITIONED: 20: region_id, 21: category_id                   |
|                                                                                 |
|   STREAM DATA SINK                                                              |
|     EXCHANGE ID: 06                                                             |
|     UNPARTITIONED                                                               |
|                                                                                 |
|   5:Project                                                                     |
|   |  <slot 11> : 21: category_id                                                |
|   |  <slot 15> : 20: region_id                                                  |
|   |  <slot 18> : 24: sum                                                        |
|   |                                                                             |
|   4:AGGREGATE (merge finalize)                                                  |
|   |  output: sum(24: sum)                                                       |
|   |  group by: 20: region_id, 21: category_id                                   |
|   |                                                                             |
|   3:EXCHANGE                                                                    |
|                                                                                 |
| PLAN FRAGMENT 2                                                                 |
|  OUTPUT EXPRS:                                                                  |
|   colocate exec groups: ExecGroup{groupId=1, nodeIds=[0, 1, 2]}                 |
|   PARTITION: RANDOM                                                             |
|                                                                                 |
|   STREAM DATA SINK                                                              |
|     EXCHANGE ID: 03                                                             |
|     HASH_PARTITIONED: 20: region_id, 21: category_id                            |
|                                                                                 |
|   2:AGGREGATE (update serialize)                                                |
|   |  STREAMING                                                                  |
|   |  output: sum(22: revenue)                                                   |
|   |  group by: 20: region_id, 21: category_id                                   |
|   |                                                                             |
|   1:Project                                                                     |
|   |  <slot 20> : 20: region_id                                                  |
|   |  <slot 21> : 21: category_id                                                |
|   |  <slot 22> : 22: revenue                                                    |
|   |                                                                             |
|   0:OlapScanNode                                                                |
|      TABLE: mv_daily_revenue                                                    |
|      PREAGGREGATION: ON                                                         |
|      partitions=4/19                                                            |
|      rollup: mv_daily_revenue                                                   |
|      tabletRatio=32/32                                                          |
|      tabletList=19896,19898,19900,19902,19904,19906,19908,19910,19919,19921 ... |
|      cardinality=1638000                                                        |
|      avgRowSize=0.0                                                             |
|      MaterializedView: true                                                     |
+---------------------------------------------------------------------------------+
56 rows in set (0.05 sec)

 

Проектирование Online-хранилищ данных на StarRocks.

Код курса
STAR
Ближайшая дата курса
7 сентября, 2026
Продолжительность
24 ак.часов
Стоимость обучения
76 800

 

Инкрементальное обновление по партиции

Покажем главное преимущество перед полным ETL. Досыпаем новые заказы только в один месяц и обновляем именно эту партицию представления. StarRocks не трогает остальные месяцы, потому что их исходные данные не менялись. Режим WITH SYNC MODE заставляет команду дождаться завершения, чтобы результат было видно сразу.

-- новые заказы в июне
INSERT INTO ods_orders VALUES
  (999000001, 12345, '2025-06-15', 'paid'),
  (999000002, 12346, '2025-06-16', 'paid');

-- обновляем только партицию июня, синхронно
REFRESH MATERIALIZED VIEW shop.mv_daily_revenue
PARTITION START ("2025-06-01") END ("2025-07-01")
WITH SYNC MODE;

-- в истории у этого запуска последняя задача выполнялась 11 секунд хотя последние 4 выполнявшиеся с интервалом 10 минут занимали 0 секунд
SELECT task_name, state, create_time, finish_time
FROM information_schema.task_runs
ORDER BY create_time DESC
LIMIT 5;

Инкрементальное обновление по партиции для StarRocks MV

 

Эмуляция сбоя и восстановление

Отдельно важно, как витрина ведет себя при сбое обновления. Если очередной refresh упал, представление остается с данными прошлого успешного прогона. Витрина не бьется и не показывает пустоту, аналитика продолжает работать на последних валидных данных. Причину падения смотрят в поле error_message истории задач, а после устранения проблемы обновление повторяют принудительно.

-- полное принудительное обновление всех партиций
REFRESH MATERIALIZED VIEW shop.mv_daily_revenue WITH SYNC MODE;

-- временно выключить и снова включить обновление по расписанию
-- ALTER MATERIALIZED VIEW shop.mv_daily_revenue INACTIVE;
-- ALTER MATERIALIZED VIEW shop.mv_daily_revenue ACTIVE;

Такое поведение снимает главный страх перед переносом логики внутрь базы. Обновление это атомарная операция на уровне партиции, а не рискованная перезапись всей витрины. При сбое достаточно перезапустить одну партицию.

 

Очистка стенда

DROP MATERIALIZED VIEW shop.mv_daily_revenue;
DROP TABLE shop.ods_orders;

 

Заключение

Асинхронные материализованные представления переносят слой витрин внутрь StarRocks и убирают зоопарк внешних скриптов. Логика витрины становится одним объектом, обновление идет по расписанию и только по изменившимся партициям, а сбой затрагивает одну партицию, а не всю витрину. В отличие от синхронных представлений ClickHouse, которые срабатывают на вставку и видят лишь новые блоки, подход StarRocks умеет пересобирать историю и не замедляет загрузку данных. Это экономит и место, и процессорное время.

Проектирование правильной архитектуры DWH со слоями и витринами мы разбираем на курсе Проектирование Online-хранилищ данных на StarRocks (код STAR). Как строить витрины с материализованными представлениями в другой аналитической СУБД, показываем на курсе по ClickHouse (код CLICH). Разобрать проектирование слоев данных вживую можно на митапе Школы Больших Данных.

 

Референсные ссылки

 

Проектирование Online-хранилищ данных на StarRocks.

Код курса
STAR
Ближайшая дата курса
7 сентября, 2026
Продолжительность
24 ак.часов
Стоимость обучения
76 800