Продолжаем тему потери данных в ClickHouse. Рекомендую прочитать первую и вторую статью цикла, чтобы не терять данные в двух других кейсах. А тема текущего обсуждения — материализованные представления и то, как можно с их участием потерять данные.
P.S. читать каждую ссылку из статьи совсем не обязательно, основные тезисы из ссылок изложены в статье.
1. Введение
Материализованные представления (далее MV) в ClickHouse отличаются от MV классических СУБД. Они больше похожи на триггеры на INSERT, что по сути является механизмом захвата изменения данных в концепции append‑only (кратко — только INSERT в совокупности с версионированием данных, никаких UPDATE/DELETE и прочих OLTP операций). Данная концепция разработки является best practice ClickHouse.
Стоит сказать, что MV — весьма надежный механизм, и если целенаправленно не совершать определенные манипуляции — то все будет работать стабильно без потерь. Как раз об этих манипуляциях мы и поговорим.
В предыдущей статье мы уже видели кейс с MV — чтение данных из Kafka и с последующей записью в конечную таблицу ClickHouse. Но, нам никто не запрещает «натравливать» MV и на другие таблицы. Давайте это и сделаем.
2. Классический пайплайн
Итак, предположим, что у нас есть; таблица test_orders
, содержащую информацию о заказах; таблица final_test_orders
, содержащая ровно такую же информацию, что и test_orders
; MV mv_final_test_orders
, которая будет просто переливать данные из test_orders
в final_test_orders
. Не будем делать никаких дополнительных манипуляций над данными, а просто перельем из одной таблички в другую.
-- создаем таблицу, в которую льются данные по заказам
drop table if exists test_orders;
create table test_orders
(
order_id String,
order_dt Datetime,
)
engine=MergeTree
order by order_id
;
-- создаем конечную таблицу
drop table if exists final_test_orders;
create table final_test_orders
(
order_id String,
order_dt Datetime,
)
engine=MergeTree
order by order_id
;
-- создаем MV, которая и будет осуществлять переливку данных
drop view if exists mv_final_test_orders;
create materialized view mv_final_test_orders
TO default.final_test_orders
(
order_id String,
order_dt Datetime,
)
AS
SELECT order_id, order_dt
FROM test_orders
;
Далее все просто - вставляем данные в таблицу test_orders
insert into test_orders values
('QWE123', '2025-03-01 12:00:00'),
('RTY456', '2025-03-01 13:00:00'),
('XYZ789', '2025-03-01 14:00:00')
;
и смотрим на результат в таблице final_test_orders
select * from final_test_orders;
Увидим то, что и ожидалось:
order_id |
order_dt |
QWE123 |
2025-03-01 12:00:00 |
RTY456 |
2025-03-01 13:00:00 |
XYZ789 |
2025-03-01 14:00:00 |
3. Ломаем пайплайн
А теперь специально сломаем конечную таблицу. Для этого пересоздаем final_test_orders
следующим образом:
drop table if exists final_test_orders;
create table final_test_orders
(
order_id UInt64,
order_dt Datetime,
)
engine=MergeTree
order by order_id
;
Ключевое изменение — order_id UInt64
, но это не сильно важно прямо сейчас. Куда важнее, что ClickHouse позволяет что угодно делать с конечной (и с исходной тоже) таблицей несмотря на активное MV, в отличие от классических СУБД. И, если в случае с Kafka Engine это еще более‑менее ок (так как офсет комитится только при успешной отработке MV), то в случае с обычной таблицей — нет. Теперь при попытке вставить данные в таблицу test_orders
MV не сможет преобразовать строку в число и INSERT закончится ошибкой:

Ожидаемо MV не смогла преобразовать строку «QWE123» к типу UInt64 (целое число). Стоит отметить, что данные в таблицу test_orders
все‑таки вставлены были (про это будет позже в пункте 4), а вот в конечную таблицу — нет.
Итак, мы поняли, что несмотря на наличие MV мы можем изменять обе таблицы как нам угодно. Это крайне неприятный момент, открывающий путь к огромному количеству кейсов «как потерять/поломать данные» (именно этим и займемся, конечно же). Но не все так плохо — мы не теряем данные в исходной таблице test_orders
и получаем на выходе ошибку, что данные не были записаны в конечную таблицу. А это значит, что полноценной потерей данных это нельзя назвать, так как мы получили уведомление. Так ведь?
4. Теряем данные
И конечно же мы можем данные полноценно потерять, иначе этой статьи и не было бы. Все просто — мы можем исключить выброс ошибки, если вставка в материализованное представление не удалась. Такая возможность присутствует по причине пункта 2 первой статьи цикла. Кратко (хотя рекомендую ознакомиться более подробно со статьей) — данные в ClickHouse вставляются блоками, и есть атомарность (гарантия) вставки именно блока данных. Вы можете сделать один мощный INSERT на миллиард записей — тогда ClickHouse под капотом разобьет все записи на n блоков (точное число не скажет никто, это зависит от многих факторов) и будет вставлять по отдельности каждый блок. Так вот, после вставки первого же блока данных запустится параллельный процесс — MV увидит, что был вставлен новый блок, а значит пора работать! Она попытается выполнить свою задачу, но выбросит ошибку. А значит прервется и основной INSERT на миллиард записей. А вставиться до выброса ошибки могли успеть далеко не все блоки данных. И получится, что данные потерялись не только в конечной таблице, но и в исходной таблице. Именно об этом и предупреждают разработчики в официальной документации:
По умолчанию, если вставка в одно из представлений завершится неудачей, то запрос INSERT также завершится неудачей, и некоторые блоки могут не быть записаны в целевую таблицу. Это можно изменить с помощью настройки
materialized_views_ignore_errors
(вы должны установить ее для запросаINSERT
), если вы установитеmaterialized_views_ignore_errors=true
, тогда любые ошибки при вставке в представления будут игнорироваться, и все блоки будут записаны в целевую таблицу.
Наиграть реальный кейс с потерей данных в исходной таблице весьма тяжело, нужно делать INSERT на миллиард записей (или танцевать с бубном вокруг настроек, отвечающих за формирование блока данных, max_block_size и т.д.).
Но, мы можем применить настройку и убедиться, что выброс ошибки происходить не будет:
insert into test_orders settings materialized_views_ignore_errors=true values
('QWE123', '2025-03-01 12:00:00'),
('RTY456', '2025-03-01 13:00:00'),
('XYZ789', '2025-03-01 14:00:00')
;

И вуаля — данные официально потеряны в конечной таблице final_test_orders
. Неприятно, но такой маневр позволяет исключить потерю данных в исходной таблице test_orders
. Шутку про пики писать не буду Мы не можем полностью исключить потерю данных, а можем лишь выбрать, где мы их будем терять. И в случае применения настройки materialized_views_ignore_errors=true
можем немного смягчить удар и начать контролировать ситуацию. Но нужны дополнительные манипуляции.
4. Контролируем потери данных
И ClickHouse не был бы ClickHouse, если бы не предоставил возможность решения проблемы, которую сам же и создал (привет дедупликации). Есть множество системных таблиц, и одна из них — system.query_views_log. Это лог работы всех MV. С ее помощью можно найти ошибки в работе MV и настроить поверх этого алертинг, что на проде крайне рекомендую сделать. Запрос, позволяющий найти ошибки, выглядит так:
select event_time, view_name, exception
from system.query_views_log
where event_date = today()
AND exception_code != 0 -- работа MV закончилась ошибкой
order by event_time desc
limit 1000;
В результате вернется время ошибки, MV и сам ошибка (ну а вообще выбираете те колонки, которые считаете нужным).

Видим именно нашу ошибку (время с предыдущим изображением отличается на 3 часа из-за UTC, и очевидно, что 10:38:00 это 13:38:00, и 13:38:00 (время ошибки MV) > 13:37:59 (время, когда делался INSERT)).
Отлично! Казалось бы, мы нашли идеальный баланс в дилемме потери данных — исключили возможность потери данных в исходной таблице, а потери в конечной стали контролируемы. Но на этом приключения не заканчиваются.
5. Неконтролируемые потери
Если постараться, то все-таки есть возможность совершить неконтролируемую потерю данных. В предыдущем примере мы вызывали именно ошибку у MV. Но, есть еще и другой механизм, а именно — вставка дефолтных значений. Цитата из оф. доки:
Материализованные представления в ClickHouse используют имена колонок вместо порядка колонок при вставке в целевую таблицу. Если некоторые имена колонок отсутствуют в результате запроса
SELECT
, ClickHouse использует значение по умолчанию, даже если колонка не является Nullable. Безопасной практикой будет добавление псевдонимов для каждой колонки при использовании Материализованных представлений.
Наиграть весьма не сложно — нужно просто допустить «рассинхрон» колонок. Для этого пересоздаем конечную таблицу так (а в случае, если колонка не является ключом - достаточно просто переименовать):
drop table if exists final_test_orders;
create table final_test_orders
(
orders String,
order_dt Datetime,
)
engine=MergeTree
order by orders
;
Ключевое — orders String
. Раньше колонка имела название order_id
. MV будет пытаться вставить в колонку order_id
, так как в ней мы это четко прописывали ранее. И так как не обнаружит order_id
— то просто вставит в orders
дефолтные значения. Итак, выполняем сперва вставку данных
insert into test_orders values
('QWE123', '2025-03-01 12:00:00'),
('RTY456', '2025-03-01 13:00:00'),
('XYZ789', '2025-03-01 14:00:00')
;
Она завершится БЕЗ ошибок даже без настройки settings materialized_views_ignore_errors=true
. Затем смотрим результат в таблице final_test_orders

И видим пустоту в колонке orders. Это неконтролируемая потеря данных, в таком случае поможет только серьезный Data Quality, средствами ClickHouse это не исправить.
6. Заключение
Материализованные представления — опасный инструмент, если не знать особенностей. Главная же (и, по сути, единственная) опасность в том, что ClickHouse позволяет совершать манипуляции как над исходной, так и над конечной таблицей при активной MV (на момент написания статьи это так). Всегда помните следующее:
Автор большой фанат ClickHouse и хочет, чтобы ClickHouse не только не тормозил, но и не терял данные.
MV — неконтролируемый ETL‑процесс.
При создании MV явно указывайте колонки и убедитесь, что такие колонки есть в конечной таблице.
После создания MV убедитесь, что она работает корректно и льет правильные данные в конечную таблицу.
Любые манипуляции над таблицами на вашей совести — предварительно убедитесь, что на таблицу не натравлена MV.
Натравите алертинг на system.query_views_log.
materialized_views_ignore_errors=true
без алертинга наsystem.query_views_log
будет врагом, а не другом.
Пользуйтесь на здоровье!