Продолжаем тему потери данных в 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_ordersMV не сможет преобразовать строку в число и 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 будет врагом, а не другом.

Пользуйтесь на здоровье!

Комментарии (0)