Перейти к содержанию

Материализованные представления и повторы

Отчёты «Событий» уже читают логический слой через FINAL. При большом потоке повторное группирование всей истории может стать дорогим. Материализованное представление позволяет подготовить часть вычислений при вставке, однако оно не превращает повторы в уникальные действия автоматически. Разберём этот риск до создания агрегата.

Результат урока — небольшой агрегат доставок и отдельно корректный снимок логических событий. Мы не запускали DDL и не измеряли ускорение. Наличие materialized view в исходнике не является подтверждением производительности, а ожидаемые значения сохраняют различие между физическими строками и действиями читателя.

Представление реагирует на вставленный блок

Incremental materialized view выполняет запрос для новых вставляемых данных и записывает результат в целевую таблицу. Оно не перечитывает всю исходную таблицу при каждом запросе dashboard. Поэтому удобно заранее сгруппировать доставки по дню и имени события.

Создайте отдельную таблицу, название которой не обещает уникальные события. Полный фрагмент sql/02-delivery-mv.sql предназначен пустому учебному окружению до вставки данных в deliveries.

CREATE TABLE IF NOT EXISTS events_lab.delivery_daily
(
  day Date,
  event_name LowCardinality(String),
  deliveries UInt64
)
ENGINE = SummingMergeTree
ORDER BY (day, event_name);

CREATE MATERIALIZED VIEW IF NOT EXISTS events_lab.delivery_daily_mv
TO events_lab.delivery_daily AS
SELECT toDate(occurred_at) AS day, event_name, count() AS deliveries
FROM events_lab.deliveries
GROUP BY day, event_name;

SummingMergeTree складывает числовые значения при объединении. При чтении всё равно используем явный sum(deliveries) с группировкой, чтобы не зависеть от завершённости фонового объединения. Одинаковый ключ может присутствовать в нескольких частях после разных пакетов.

SELECT day, event_name, sum(deliveries) AS deliveries
FROM events_lab.delivery_daily
GROUP BY day, event_name
ORDER BY day, event_name;

Ожидаем, что повторная строка завершения увеличит этот агрегат. Так и должно быть: он считает доставки. Документация incremental materialized views описывает обработку вставляемого блока. Наше название показателя отражает именно эту семантику.

Почему FINAL внутри view не исправляет историю

Возникает соблазн читать events FINAL прямо в materialized view и назвать результат уникальными событиями. Но view обрабатывает вставленный блок, а не весь накопленный исторический набор, который позднее объединит ReplacingMergeTree. Повтор в другом пакете может снова увеличить агрегат, а последующая замена строки не отменит уже записанный вклад.

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

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

Снимок из логической таблицы

Создадим таблицу снимка и загрузим в неё агрегат из logical_events. В учебной локальной среде можно очистить снимок и заполнить заново; рабочий dashboard не должен читать частично заполненную таблицу в процессе такой операции.

CREATE TABLE IF NOT EXISTS events_lab.event_daily_snapshot
(
  day Date,
  lesson_id LowCardinality(String),
  event_name LowCardinality(String),
  events UInt64
)
ENGINE = MergeTree
ORDER BY (day, lesson_id, event_name);

INSERT INTO events_lab.event_daily_snapshot
SELECT toDate(occurred_at), lesson_id, event_name, count()
FROM events_lab.logical_events
GROUP BY toDate(occurred_at), lesson_id, event_name;

Это первоначальное заполнение пустой таблицы. Повторная вставка без очистки удвоит снимок, потому что обычный MergeTree не заменяет агрегат. Для внешнего проекта потребуются новая таблица-снимок и согласованное переключение, либо другая управляемая стратегия обновления. В этой серии такие операции не выполняются и не маскируются словами «автоматически обновится».

Позднее событие и окно пересчёта

После получения завершения b нужно пересчитать первый октябрь, хотя текущая дата уже пятая. Если обновлять только сегодняшние строки, исторический отчёт останется неверным. Поэтому стратегия пересчёта опирается на допустимую задержку доставки и затронутые даты действия.

Можно поддерживать очередь периодов, которые требуют обновления, или пересчитывать скользящий хвост. В обоих случаях важен договор о событиях, пришедших старше хвоста. Их нельзя молча принять в логический слой и не отразить в dashboard. Либо период повторно строится, либо событие обрабатывается по явно заданной политике.

Материализованный агрегат доставок такого пересчёта не требует для новых вставок: поздняя строка увеличит день действия. Но повторная строка тоже увеличит его. Два свойства связаны с тем, что он считает входящие блоки, а не окончательно выбранные события.

Заполнение старой истории

Созданное после загрузки view не делает прежние строки частью результата само по себе. Для существующей истории нужна отдельная backfill-операция. Если одновременно идут новые вставки, легко посчитать одну строку дважды или пропустить границу. Поэтому заполнение должно иметь согласованную точку разделения старых и новых данных.

Официальное руководство backfilling рассматривает отдельное заполнение и связь с materialized views. Наш маленький пакет позволяет выбрать самый простой порядок: сначала создать пустую схему с view, затем загрузить файл один раз. Другой порядок требует явно описанного переноса.

Состояния уникального счёта не складывают готовые числа

Если каждый пакет содержит своё число уникальных браузеров, суммирование этих чисел завысит общий результат: один браузер может быть в нескольких пакетах. Для объединения уникальности нужны состояния агрегатных функций или пересчёт исходного набора, а не сложение уже завершённых результатов.

ClickHouse предоставляет AggregatingMergeTree и пары функций состояния и объединения. Их применение требует согласованных типов и ключей группировки. В этой серии они не внедрены в основной поток, потому что вопрос повторов события и точной уникальности посетителя нуждается в отдельном разборе. Нельзя заменить UInt64 на сложное состояние без изменения запросов чтения.

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

Если требуется ускорить воронку или удержание, обычной суммы событий недостаточно. Понадобятся порядок, даты и связь с участником. Предварительное агрегирование должно сохранить те сведения, из которых ещё можно ответить на вопрос. Без этого быстрый слой станет источником красивого, но другого отчёта.

На текущем шаге вы видите, что предварительная агрегация меняет не только скорость, но и доступный смысл. Следующий урок добавит TTL и сроки хранения. Удаление исходных данных тоже не пересчитывает все агрегаты автоматически, поэтому срок каждого слоя понадобится определить отдельно.

Оглавление серии