
Нет, клик не превратится в этого монстра. В него превратитесь вы, если не будете знать того, о чем эта статья.
Про особенность хранения данных в клике сказано многое, но сегодня мы взглянем своими на глазами на то, как один элементарный запрос на изменение одной строки может практически убить сервер.
Для начала стоит сказать, что мутации - это механизм, через который реализовано изменение, удаление, добавление данных (DML). Один из них вполне безобидный, а вот два других могут доставить много проблем. И так, начнем с безобидного, но подводящего к краю пропасти.
P.S. про куски данных и слияния говорить не будем, об этом уже сказано много, в т.ч. в документации, которую оставил выше. Мы же в статье займемся экспериментами, а не теорией.
Добавление новых данных
Имеется: клик, поднятый в докере, версия 25.1.2.3 (в последних все то же самое, и это будет всегда неизменным). Создадим таблицу t1 с одной колонкой id и двумя записями - 1 и 2.
create table t1 engine=MergeTree order by id as (select 1 as id union all select 2 as id);
Получаем такую табличку:

А теперь посмотрим, где данные физически хранятся. В этом поможет системная таблица system.parts и колонка path:
select path from system.parts where table = 't1';
Видим путь, и мы должны идти по нему:

Вот, что по этому пути располагается:

Директория detached - это "отсоединенные" куски данных. Нас это не сильно волнует, в реальных условиях эта директория, как правило, пустует. Куда важнее директория all_1_1_0, являющаяся куском данных и содержащая множество файлов, среди которых главный - data.bin
Именно в нем физически хранятся данные. Остальные файлы в рамках изучения мутаций нас не интересуют.

Теперь, когда мы увидели, где и как хранятся данные, вставим новую строку:
insert into t1 values(3);
И взглянем в system.parts:

Видим, что теперь два пути! Появился новый all_2_2_0. Смотрим:

Это точно такая же директория, как и рассмотренная ранее all_1_1_0. В ней также есть файл data.bin
А теперь щепотка магии - мы можем прочитать только данные из интересующего нас куска данных, чтобы убедиться, что в all_2_2_0 только запись со значением 3. В этом поможет магическая колонка _part:

Видим, что в этом куске данных действительно только та запись, которую мы вставили.
Фух, сколько нового для тех, кто никогда этого не делал. Но необходимо сделать вывод: когда мы вставили всего лишь одну строку - создалась целая директория с кучей файлов, главный из которых - data.bin
А что будет дальше? А если мы сделаем 1 000 000 инсертов?
Да, это те вопросы, вокруг которых произрастает множество проблем и концепций работы с кликом.
Да, мы не может делать 1 000 000 инсертов в секунду. Да даже за пять минут мы столько сделать не сможем. И вот почему: раз в n времени (факторов очень много, в рамках статьи это рассматривать нет смысла) клик в фоновом режиме будет производить слияния кусков данных. То есть, он залезет в директорию all_1_1_0 и all_2_2_0, возьмет файлы data.bin из каждой директории, соединит их в один новый data.bin и создаст вокруг него кучу вспомогательных файлов (индекс, засечки и т.д.). Это и будет новый кусок данных, получившийся в результате слияния двух других кусков. А старые куски данных all_1_1_0 и all_2_2_0 пометит как неактивные, и чистильщик мусора когда-нибудь (раз в 8 минут вроде) удалит их.
Вот такой сложный процесс происходит под капотом. Но именно он позволяет вставлять в клик хоть триллион записей за раз (если диск позволяет) без блокировок.
Что ж, давайте убедимся, что я не соврал. Вызовем принудительное слияние кусков данных:
optimize table t1;
И вот теперь взглянем на system.parts, но добавим колонку active (активный кусок данных или нет):

Видим, что появился новый кусок данных all_1_2_1. У него флаг active=1, а у двух других кусков, из которых он и был "слеплен", флаг стал равен 0. И чистильщик мусора когда-нибудь их удалит. Ах да, принудительно его вызвать, в отличие от процесса слияния кусков, нельзя.
А теперь давайте под резюмируем простым языком: у нас был один файлик с данными, мы вставили в таблицу одну строку, под это дело создался новый файлик с данными, который потом объединился со старым в один новый актуальный файлик а старые два удалились. Сложно, но вроде бы ничего критичного. Настораживает разве что тот момент, когда данные слились в один новый кусок, а старые куски еще не удалились. Да, в этот момент мы действительно физически храним данные дважды - в двух старых кусках и одном новом. Фактически имеем дублирование по диску.
Но это не сильно страшно. Гораздо страшнее ответ на другой вопрос: А что будет, если мы захотим изменить/удалить хотя бы одну существующую строку?
Изменение/удаление данных
Этот процесс тоже реализован через мутации. То есть, создастся новый кусок данных. А как же клик это сделает? - правильно, он будет вынужден:
прочитать все куски данных (если нет индекса)
найти те куски данных, в которых есть строка, подлежащая изменению
изменить эту строку
все остальные строки из куска/кусков взять без изменений
соединить эти строки воедино в рамках работы каждым кусок
создать из них кучу новых кусков данных
Как вы понимаете - это очень ресурсоемкий процесс. Давайте к примеру. Создадим новую таблицу t2, в которой будет две колонки - id и name. id будет в индексе, name - нет. Нужно это по той причине, что в клике нельзя менять значение колонки, являющейся индексом. Поэтому мы будем менять значение колонки name в зависимости от условий по колонке id. Создаем таблицу:
create table t2 engine=MergeTree order by id as ( select 1 as id, 'qq' as name union all select 2 as id, 'bb' as name );

А теперь изменим qq на xx:
alter table t2 update name = 'xx' where id = 1;
Видим, что стало два куска данных, один из которых, ожидаемо, не активен:

Мы убедились, что фактически было произведено полное копирование всей таблицы (ну ладно-ладно, не все таблицы на проде хранятся одним куском, но все же) ради изменения одной строки! Кощунство, кто ж спорит. И это один из подводных камней клика, который обязательно нужно знать, дабы горя не знать.
А теперь попробуем следующее: добавим строку с id=3 и name = 'pp':
insert into t2 values(3, 'pp');
Видим, ожидаемо, два активных куска данных:

А теперь изменим данные так, чтобы зацепить оба активных куска данных. Напоминаю, t2 имеет следующее содержимое:

Поменяем все и везде (id != 0):
alter table t2 update name = 'aa' where id != 0;
И видим страшное:

Теперь у нас два активны куска данных. Почему, спросите вы? - потому, что мутации производятся отдельно над каждым куском данных. То есть, клик не читал разом все куски данных, объединяя затем их сразу в финальный большой кусок. Это правильное поведение (ну разумеется в рамках заложенной концепции кусков данных и их слияний), так как кусков может быть несколько тысяч (максимум ~3 000, если вы не дай бог не увеличили это значение, даже не буду говорить как это сделать), и их разовое чтение для слияния может быть фатальным.
Какие проблемы это порождает?
Дудос файловой системы (помимо дудоса I/O, конечно же). Всего лишь одна операция по изменению записей, цепляющих множество кусков данных, может породить ТЫСЯЧИ новых директорий и ДЕСЯТКИ ТЫСЯЧ новых файлов. Все зависит от размера таблицы, частоты вставок данных и т.д. Если за 8 минут (периодичность чистильщика мусора) успели сгенерировать парочку ТБ и парочку тысяч новых кусков данных - готовьтесь с коллегами к перекуру в лучшем случае. В худшем - к починке сервера.
МИНИМУМ Х2 к хранению. Почему минимум? - потому, что вам никто не мешает сделать alter много-много раз. И каждый alter - дублирование куска данных целиком, в отличие от insert. Забить диск на 100% - 5 минут делов.
Заключение
Мутации - один из тех подводных камней, о который страшно удариться. Но теперь вы не ударитесь.
Как вы уже поняли, ClickHouse - штука мощная, но со своим характером.
Осваивать этого зверя можно двумя способами. Первый - героический: месяцами вчитываться в документацию, собирать грабли по крупицам из форумов (привет Хабр) и всевозможных чатов. Второй - прагматичный: пройти бесплатный курс от автора этой статьи, где все шишки уже набиты, а опыт упакован в понятные уроки.
Выбирайте путь умного.
ClickHouse: быстрый старт
P.S. а о том, как правильно изменять данные в клике, расскажу в следующих статьях. Но вы можете не ждать их и изучить это уже сейчас в курсе.
