Learning Platform
Глоссарий Troubleshooting
Урок 05.03 · 20 мин
Продвинутый
Materialized ViewPitfallsBackfillingDistributed MVDeduplicationALTER TABLE

Подводные камни MV

Materialized Views в ClickHouse работают надёжно, но имеют несколько неочевидных ограничений. Каждый из этих pitfalls может привести к потере данных, дублированию или некорректным результатам в production. Разберём пять критических проблем и способы их решения.


Pitfall 1: Backfilling — данные до создания MV

MV не обрабатывает данные, вставленные до своего создания. MV — это триггер на INSERT, а не ретроспективный запрос. Если source-таблица содержит 10 миллионов строк, а вы создали MV после — target-таблица будет пуста.

Backfilling: MV не видит исторические данные
10M строк (до MV)Данные до создания MV: 10 млн строк уже в source-таблице. MV не имеет механизма их обработки -- триггер срабатывает только на новые INSERT.
MV не видитMV не срабатывает: данные вставлены ДО создания MV. Триггер не может быть применён ретроспективно. Target-таблица остаётся пуста для этих строк.
после CREATE MV
Новые INSERTНовые INSERT после создания MV: эти данные обрабатываются MV корректно. Каждый новый INSERT триггерит MV SELECT.
MV обрабатываетMV срабатывает: новые INSERT блоки проходят через MV SELECT и результат записывается в 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;
WARNING

При 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 не произошёл
TIP

insert_deduplication включен по умолчанию для ReplicatedMergeTree. Для обычного MergeTree дедупликация INSERT недоступна — используйте идемпотентную логику на стороне клиента.


Сводная таблица pitfalls

5 подводных камней MV
1. BackfillingBackfilling: MV не обрабатывает данные, вставленные ДО создания MV. Решение: INSERT INTO target SELECT ... FROM source с теми же -State функциями.
2. ALTER TABLEALTER TABLE: изменения схемы и мутации (UPDATE/DELETE) не триггерят MV. Решение: пересоздать MV + backfill target.
3. Distributed MVsDistributed MVs: MV выполняется локально на каждом шарде. INSERT через Distributed-таблицу может привести к дубликатам при resharding. Решение: INSERT в локальные таблицы.
4. Engine mismatchEngine mismatch: каскадные MV с ReplacingMergeTree/CollapsingMergeTree в промежуточных таблицах видят данные до merge. Решение: AggregatingMergeTree для каскадов.
5. DeduplicationДедупликация: MV не дедуплицирует INSERT retry. Решение: ReplicatedMergeTree с insert_deduplication для source-таблицы.
Принцип: триггер на INSERTОбщий принцип: MV -- это триггер на INSERT block. Все pitfalls следуют из этого: MV не видит прошлое, не реагирует на ALTER, работает локально, обрабатывает данные до merge.
WAL и INSERT: как PostgreSQL гарантирует durability MVCC: xmin и xmax — видимость строк

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Инженер вставил 1 миллион строк в source-таблицу, затем создал MV с TO clause. Target-таблица пуста. Почему и как исправить?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7