Подводные камни MV
Materialized Views в ClickHouse работают надёжно, но имеют несколько неочевидных ограничений. Каждый из этих pitfalls может привести к потере данных, дублированию или некорректным результатам в production. Разберём пять критических проблем и способы их решения.
Pitfall 1: Backfilling — данные до создания MV
MV не обрабатывает данные, вставленные до своего создания. MV — это триггер на INSERT, а не ретроспективный запрос. Если source-таблица содержит 10 миллионов строк, а вы создали MV после — target-таблица будет пуста.
Решение: ручной backfill
-- После создания MV: ручная вставка исторических данных в target
INSERT INTO daily_stats
SELECT
toDate(timestamp) AS date,
sumState(1) AS visits,
uniqState(user_id) AS users
FROM raw_events
WHERE timestamp < '2024-06-01' -- данные до создания MV
GROUP BY date;
При backfill используйте те же -State функции, что и в MV SELECT. Если MV записывает sumState(), backfill тоже должен использовать sumState(), а не sum().
Pitfall 2: ALTER TABLE не триггерит MV
ALTER TABLE source_table ADD COLUMN, DROP COLUMN, MODIFY COLUMN — ни одна из этих операций не вызывает пересчёт MV. ALTER TABLE — это мутация схемы, а не INSERT.
-- Добавляем столбец в source
ALTER TABLE raw_events ADD COLUMN region String DEFAULT 'unknown';
-- MV НЕ пересчитывается. Target-таблица не знает о region.
-- Если MV SELECT использует region, нужно:
-- 1. DROP MV
-- 2. Пересоздать MV с новым SELECT
-- 3. Backfill target-таблицу
Более тонкий случай: ALTER TABLE UPDATE обновляет данные в source через мутацию. MV не срабатывает — мутация это фоновая перезапись parts, а не INSERT.
-- Обновление данных в source -- MV НЕ срабатывает
ALTER TABLE raw_events UPDATE event_type = 'purchase'
WHERE event_type = 'buy';
-- Target-таблица по-прежнему содержит агрегаты со старым event_type='buy'
Решение: После ALTER TABLE UPDATE пересчитайте target вручную или через TRUNCATE + backfill.
Pitfall 3: Distributed MVs
В кластерном ClickHouse MV выполняется локально на каждом шарде. Если вставка идёт через Distributed-таблицу, данные распределяются по шардам, и MV на каждом шарде обрабатывает свою порцию.
Проблема возникает, когда INSERT идёт через Distributed-таблицу, а MV привязана к локальной таблице:
-- На каждом шарде:
CREATE TABLE raw_events_local (...) ENGINE = MergeTree() ...;
CREATE TABLE daily_stats_local (...) ENGINE = SummingMergeTree() ...;
CREATE MATERIALIZED VIEW mv_local TO daily_stats_local AS
SELECT ... FROM raw_events_local GROUP BY ...;
-- Distributed-таблица для записи:
CREATE TABLE raw_events_dist AS raw_events_local
ENGINE = Distributed(cluster, db, raw_events_local, rand());
Правило: вставляйте в локальные таблицы, не через Distributed. При INSERT в Distributed, данные маршрутизируются на шард, затем MV на том шарде обрабатывает блок. Это работает, но если маршрутизация изменится (resharding), target может получить дубликаты или пропуски.
Решение: Используйте INSERT через локальные таблицы. Для чтения агрегатов создайте Distributed-таблицу поверх daily_stats_local.
Pitfall 4: Engine mismatch в каскадных MV
Каскадные MV (MV на target другой MV) опасны с CollapsingMergeTree или ReplacingMergeTree в промежуточных таблицах:
-- ОПАСНО: cascading MV с ReplacingMergeTree
CREATE TABLE deduped (...) ENGINE = ReplacingMergeTree(version) ...;
CREATE MATERIALIZED VIEW mv1 TO deduped AS SELECT ... FROM raw;
CREATE MATERIALIZED VIEW mv2 TO final_stats AS SELECT ... FROM deduped;
Проблема: mv2 срабатывает на INSERT в deduped (до merge). ReplacingMergeTree удаляет дубликаты при merge, но MV видит данные до merge. mv2 обработает дубликат, который позже будет удалён.
Решение: Для каскадных паттернов используйте AggregatingMergeTree или SummingMergeTree, где объединение при merge не меняет семантику (сумма двух частичных сумм = полная сумма). Избегайте ReplacingMergeTree и CollapsingMergeTree в промежуточных каскадных таблицах.
Pitfall 5: Дедупликация при retry
MV не дедуплицирует INSERT. Если клиент повторяет INSERT после таймаута (не получив подтверждения), MV обработает блок дважды:
Клиент -> INSERT block -> ClickHouse (success) -> MV fires -> target получает строки
Клиент (timeout, retry) -> INSERT block -> ClickHouse -> MV fires -> target получает ДУБЛИКАТ
ReplicatedMergeTree с настройкой insert_deduplication решает эту проблему для source-таблицы: повторный INSERT с тем же hash блока игнорируется. Но это работает только для source, а не для target.
Решение:
-- Source-таблица с дедупликацией
CREATE TABLE raw_events (...)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/raw_events', '{replica}')
ORDER BY ...;
-- insert_deduplication=1 (default для ReplicatedMergeTree)
-- Повторный INSERT с тем же hash будет проигнорирован
-- MV не сработает повторно, т.к. INSERT не произошёл
insert_deduplication включен по умолчанию для ReplicatedMergeTree. Для обычного MergeTree дедупликация INSERT недоступна — используйте идемпотентную логику на стороне клиента.