Материализованные представления: предагрегация на лету
Считаем агрегаты заранее, прямо в момент вставки, — и отчёты становятся мгновенными.
Материализованное представление (materialized view) в ClickHouse — это триггер: при каждой вставке в исходную таблицу оно вычисляет агрегаты по новым строкам и дописывает их в отдельную таблицу-приёмник.
Чем оно отличается от обычного VIEW
Обычное представление — просто сохранённый запрос: каждый раз оно пересчитывает всё заново. Материализованное хранит уже посчитанный результат. В ClickHouse это работает не как «обновляемый кэш», а как триггер на INSERT: новые данные сразу превращаются в агрегированные строки приёмника.
сырые события --INSERT--> [исходная таблица]
| (триггер MV)
v
[таблица-приёмник: посуточные агрегаты]Зачем это нужно
Дашборд «уникальные пользователи и средняя длительность по дням» по миллиардам сырых событий считается долго. Но если заранее свернуть события в посуточные строки, отчёт будет читать тысячи строк вместо миллиардов — мгновенно. Материализованное представление делает эту свёртку автоматически, по мере поступления данных.
Как это собирается
Связка обычно такая: таблица-приёмник на AggregatingMergeTree хранит состояния агрегатов (помните -State из прошлого урока), а материализованное представление наполняет её при вставке:
-- Приёмник: хранит складываемые состояния по дням
CREATE TABLE events_by_day
(
day Date,
users_state AggregateFunction(uniq, UInt32),
dur_state AggregateFunction(avg, Float64)
)
ENGINE = AggregatingMergeTree
ORDER BY day;
-- MV: при вставке в events дописывает состояния в приёмник
CREATE MATERIALIZED VIEW events_by_day_mv TO events_by_day AS
SELECT
toDate(event_time) AS day,
uniqState(user_id) AS users_state,
avgState(duration) AS dur_state
FROM events
GROUP BY day;Запрос к дашборду читает приёмник и финализирует состояния через -Merge:
SELECT
day,
uniqMerge(users_state) AS users,
avgMerge(dur_state) AS avg_dur
FROM events_by_day
GROUP BY day
ORDER BY day;Как работает под капотом
Материализованное представление срабатывает на каждый вставленный блок исходной таблицы, прогоняет его через свой SELECT и пишет результат в приёмник. Оно видит только новые строки вставки, а не всю таблицу — поэтому работает быстро. Старые данные в приёмник нужно заливать отдельно (например, разовым INSERT … SELECT).
Частые ошибки
- Думать, что MV пересчитывает старое. Оно реагирует только на новые вставки; исторические данные нужно досыпать вручную.
- Хранить финальные числа вместо состояний. Для корректных уникальных и средних по разным периодам в приёмнике нужны
-State, а не готовые числа. - Тяжёлый SELECT в MV. Он выполняется на каждой вставке — слишком сложное представление замедлит загрузку.
Итоги
- Материализованное представление — триггер на вставку, а не обновляемый кэш.
- Оно сворачивает новые данные в агрегаты приёмника на лету.
- В паре с
AggregatingMergeTreeи-State/-Mergeдаёт мгновенные дашборды. - Видит только новые вставки; историю досыпают отдельно.