Materialized View в ClickHouse часто используют для быстрых отчетов. Вместо того чтобы каждый раз считать тяжелую агрегацию по сырой таблице, ClickHouse может считать часть результата при вставке данных и складывать его в отдельную target table. Это особенно полезно для дневных, часовых и продуктовых витрин

- Как materialized view работает в ClickHouse
- Source table
- Target table
- Создаем materialized view
- Проверяем работу
- Что со старыми данными
- Когда использовать AggregatingMergeTree
- Ошибки новичков
- Как безопасно внедрять materialized view
- Когда view не нужна
- Как удалить materialized view
- Что продумать перед витриной
- Как понять, что materialized view действительно нужна?
- Что чаще всего забывают при внедрении?
- Когда витрина связана с изменением данных?
Как materialized view работает в ClickHouse
В ClickHouse есть два важных понятия: source table и target table. Source table принимает исходные события. Materialized view слушает вставки в source table, выполняет заданный SELECT над новым блоком данных и записывает результат в target table
Это отличается от привычного materialized view в некоторых OLTP-СУБД, где представление периодически пересчитывают целиком. В ClickHouse инкрементальная view переносит вычисления на момент вставки
Source table
CREATE TABLE demo.events
(
event_date Date,
event_time DateTime,
user_id UInt64,
event_type LowCardinality(String),
amount Decimal(12, 2)
)
ENGINE = MergeTree
ORDER BY (event_date, event_type, user_id);
Target table
Для сумм и счетчиков удобно использовать SummingMergeTree. Он умеет суммировать числовые колонки при фоновых слияниях частей
CREATE TABLE demo.daily_revenue
(
event_date Date,
event_type LowCardinality(String),
events UInt64,
revenue Decimal(18, 2)
)
ENGINE = SummingMergeTree
ORDER BY (event_date, event_type);
Создаем materialized view
CREATE MATERIALIZED VIEW demo.daily_revenue_mv
TO demo.daily_revenue
AS
SELECT
event_date,
event_type,
count() AS events,
sum(amount) AS revenue
FROM demo.events
GROUP BY event_date, event_type;
Теперь при вставке новых строк в demo.events агрегированный результат будет попадать в demo.daily_revenue
Проверяем работу
INSERT INTO demo.events VALUES
('2026-05-19', '2026-05-19 10:00:00', 101, 'view', 0),
('2026-05-19', '2026-05-19 10:01:00', 101, 'purchase', 990),
('2026-05-19', '2026-05-19 10:02:00', 102, 'purchase', 1490);
SELECT
event_date,
event_type,
sum(events) AS events,
sum(revenue) AS revenue
FROM demo.daily_revenue
GROUP BY event_date, event_type
ORDER BY event_date, event_type;
Даже если используется SummingMergeTree, в запросах часто оставляют финальный GROUP BY и sum(). Фоновые слияния асинхронные, поэтому в таблице могут временно лежать несколько частичных строк для одного ключа
Что со старыми данными
Важный момент: materialized view обычно обрабатывает новые вставки после своего создания. Если в source table уже были исторические данные, target table нужно заполнить отдельно:
INSERT INTO demo.daily_revenue
SELECT
event_date,
event_type,
count() AS events,
sum(amount) AS revenue
FROM demo.events
GROUP BY event_date, event_type;
Перед такой загрузкой проверьте, что не продублируете данные, которые view уже успела записать
Когда использовать AggregatingMergeTree
Если нужны сложные агрегаты, например уникальные пользователи, квантили или состояния агрегатных функций, смотрите в сторону AggregatingMergeTree. Он хранит агрегатные состояния, а не простые числа. Это мощнее, но сложнее для новичка
Ошибки новичков
- Ожидать пересчета всей истории. Новая view не магически обработает старые строки без отдельной вставки.
- Создать десятки views на одну таблицу. Каждая view добавляет работу при insert.
- Забыть target table. Для серьезных схем лучше явно создавать таблицу назначения.
- Не агрегировать при чтении SummingMergeTree. Фоновые merge-процессы асинхронные.
- Класть слишком тяжелую логику на вставку. Это может замедлить ingestion.
Как безопасно внедрять materialized view
Не создавайте новую view сразу на боевой поток, если запрос сложный. Сначала проверьте SELECT отдельно на историческом срезе. Затем создайте target table под другим именем, заполните ее небольшой партией и сравните результат с прямым запросом по source table
-- Контрольный прямой расчет
SELECT event_date, event_type, count(), sum(amount)
FROM demo.events
WHERE event_date = '2026-05-19'
GROUP BY event_date, event_type;
-- Проверка витрины
SELECT event_date, event_type, sum(events), sum(revenue)
FROM demo.daily_revenue
WHERE event_date = '2026-05-19'
GROUP BY event_date, event_type;
Если значения не совпали, не спешите чинить view на production. Проверьте, не продублировали ли вы backfill, правильно ли выбрали агрегирующий движок и не забыли ли финальную агрегацию при чтении
Когда view не нужна
- Запрос выполняется редко и уже работает достаточно быстро.
- Пользователи постоянно меняют группировки и фильтры.
- Вставка данных важнее скорости отчета, а view сильно замедляет ingestion.
- Сырые данные маленькие, и обычный GROUP BY справляется.
Как удалить materialized view
DROP TABLE demo.daily_revenue_mv;
Это удалит саму view. Target table останется, если вы создавали ее отдельно:
DROP TABLE demo.daily_revenue;
Что продумать перед витриной
Как понять, что materialized view действительно нужна?
Посмотрите на повторяемость запроса. Если дашборд постоянно считает одни и те же дневные метрики, витрина уместна. Если аналитик каждый раз меняет срезы и условия, сначала попробуйте обычные функции ClickHouse и нормальный ORDER BY
Что чаще всего забывают при внедрении?
Старые данные. Инкрементальная view обработает новые вставки, но историю нужно переложить отдельно. Перед этим проверьте source table: ее схема и ключ сортировки должны быть понятны, как в уроке CREATE TABLE в ClickHouse
Когда витрина связана с изменением данных?
Когда нужно пересобрать агрегаты после исправления источника, удаления партии или изменения правил расчета. Тогда рядом пригодится материал UPDATE, DELETE, TTL и ReplacingMergeTree в ClickHouse, чтобы не запускать тяжелые мутации вслепую



