ReplacingMergeTree
В то время как транзакционные базы данных оптимизированы для выполнения транзакций обновления и удаления, OLAP базы данных предоставляют меньшие гарантии для таких операций. Вместо этого они оптимизируют работу с неизменяемыми данными, которые вставляются пакетами, что позволяет значительно ускорить аналитические запросы. Хотя ClickHouse поддерживает операции обновления через мутации, а также легковесный способ удаления строк, его колоночная структура подразумевает, что эти операции должны выполняться с осторожностью, как описано выше. Эти операции обрабатываются асинхронно, выполняются в одном потоке и требуют (в случае обновлений) переписывания данных на диск. Поэтому их не следует использовать для большого количества мелких изменений. Для обработки потока обновлений и удалений строк, избегая вышеперечисленных паттернов использования, мы можем использовать движок таблиц ClickHouse ReplacingMergeTree.
Автоматические обновления вставленных строк
Движок ReplacingMergeTree позволяет выполнять операции обновления на строках без необходимости использовать неэффективные операторы ALTER
или DELETE
, предоставляя пользователям возможность вставлять несколько копий одной и той же строки и отмечать одну из них как последнюю версию. В фоновом режиме, в свою очередь, асинхронно удаляются более старые версии одной и той же строки, эффективно имитируя операцию обновления с использованием неизменяемых вставок.
Это основано на возможности движка таблицы идентифицировать дублирующиеся строки. Это достигается за счет использования оператора ORDER BY
для определения уникальности, т.е. если две строки имеют одинаковые значения для колонок, указанных в ORDER BY
, они считаются дублирующимися. Столбец version
, указанный при определении таблицы, позволяет сохранять последнюю версию строки, когда две строки идентифицируются как дубликаты, т.е. сохраняется строка с наибольшим значением version
.
Мы иллюстрируем этот процесс в приведенном ниже примере. Здесь строки уникально определяются по колонке A (оператор ORDER BY
для таблицы). Предполагаем, что эти строки были вставлены в два пакета, что привело к образованию двух частей данных на диске. Позже, во время асинхронного фонового процесса, эти части объединяются.
ReplacingMergeTree дополнительно позволяет указать колонку удалений. Она может принимать значения 0 или 1, где значение 1 указывает, что строка (и её дубликаты) были удалены, а ноль используется в противном случае. Примечание: Удаленные строки не будут удалены во время слияния.
Во время этого процесса при слиянии частей происходят следующие события:
- Строка, идентифицированная значением 1 для колонки A, имеет как обновленную строку с версией 2, так и удаленную строку с версией 3 (и значением колонки удалений равным 1). Соответственно, последняя строка, помеченная как удаленная, сохраняется.
- Строка, идентифицированная значением 2 для колонки A, имеет две обновленные строки. Последняя строка сохраняется со значением 6 для колонки цены.
- Строка, идентифицированная значением 3 для колонки A, имеет строку с версией 1 и удаленную строку с версией 2. Эта удаленная строка сохраняется.
В результате этого процесса слияния у нас есть четыре строки, представляющие окончательное состояние:

Обратите внимание, что удалённые строки никогда не удаляются. Их можно принудительно удалить с помощью OPTIMIZE table FINAL CLEANUP
. Это требует включения экспериментальной настройки allow_experimental_replacing_merge_with_cleanup=1
. Это следует делать только при выполнении следующих условий:
- Вы можете быть уверены, что после выполнения операции не будут вставлены строки со старыми версиями (для тех, которые удаляются с помощью очистки). Если они будут вставлены, их неправильно сохранит система, так как удаленные строки больше не будут присутствовать.
- Убедитесь, что все реплики синхронизированы до выполнения очистки. Это можно сделать с помощью команды:
Мы рекомендуем приостановить вставки после того, как (1) будет гарантировано и до завершения этой команды и последующей очистки.
Обработка удалений с помощью ReplacingMergeTree рекомендуется только для таблиц с низким или умеренным числом удалений (менее 10%), если не могут быть запланированы периодические очистки при соблюдении вышеперечисленных условий.
Совет: пользователи также могут иметь возможность выполнить
OPTIMIZE FINAL CLEANUP
для выборочных партиций, которые больше не подлежат изменению.
Выбор первичного/дедупликационного ключа
Выше мы выделили важное дополнительное ограничение, которое также должно быть выполнено в случае ReplacingMergeTree: значения колонок в ORDER BY
уникально идентифицируют строку при изменениях. Если вы мигрируете с транзакционной базы данных, такой как Postgres, оригинальный первичный ключ Postgres должен быть включён в оператор ORDER BY
ClickHouse.
Пользователи ClickHouse знакомы с выбором колонок для своего оператора ORDER BY
, чтобы оптимизировать производительность запросов. Обычно эти колонки следует выбирать на основе ваших часто используемых запросов и перечислять их в порядке возрастания кардинальности. Важно отметить, что ReplacingMergeTree накладывает дополнительное ограничение - эти колонки должны быть неизменяемыми, т.е. если вы реплицируете данные из Postgres, добавляйте колонки в этот оператор только если они не изменяются в исходных данных Postgres. Хотя другие колонки могут изменяться, эти должны оставаться последовательными для уникальной идентификации строк.
Для аналитических нагрузок первичный ключ Postgres обычно имеет небольшую ценность, так как пользователи редко выполняют точечные поиски строк. Учитывая, что мы рекомендуем упорядочивать колонки по возрастанию кардинальности, а также то, что совпадения на колонках, перечисленных ранее в ORDER BY, обычно будут более быстрыми, первичный ключ Postgres следует добавлять в конец ORDER BY
(если он не имеет аналитической ценности). В случае, если несколько колонок формируют первичный ключ в Postgres, их следует добавлять в ORDER BY
, соблюдая кардинальность и вероятность значений запроса. Пользователи также могут захотеть сгенерировать уникальный первичный ключ с использованием конкатенации значений через материализуемую колонку MATERIALIZED
.
Рассмотрим таблицу постов из набора данных Stack Overflow.
Мы используем ключ ORDER BY
из (PostTypeId, toDate(CreationDate), CreationDate, Id)
. Колонка Id
, уникальная для каждого поста, обеспечивает возможность дедупликации строк. Колонки Version
и Deleted
добавлены в схему по мере необходимости.
Запросы к ReplacingMergeTree
Во время слияния ReplacingMergeTree идентифицирует дублирующиеся строки, используя значения колонок ORDER BY
в качестве уникального идентификатора, и либо сохраняет только наивысшую версию, либо удаляет все дубликаты, если последняя версия указывает на удаление. Однако это обеспечивает лишь конечную корректность - это не гарантирует, что строки будут дедуплицированы, и на это нельзя полагаться. Таким образом, запросы могут давать некорректные ответы из-за того, что обновления и удаления строк учитываются в запросах.
Чтобы получить корректные ответы, пользователям необходимо дополнить фоновые слияния дедупликацией и удалением на момент запроса. Это можно сделать с помощью оператора FINAL
.
Рассмотрим таблицу постов выше. Мы можем использовать обычный метод загрузки этого набора данных, но указать колонки удалений и версии с дополнительными значениями 0. Для примера мы загружаем только 10000 строк.
Давайте подтвердим количество строк:
Теперь мы обновим статистику ответов на наши посты. Вместо обновления этих значений мы вставляем новые копии 5000 строк и увеличиваем на единицу их номер версии (это означает, что в таблице будет существовать 150 строк). Мы можем смоделировать это с помощью простого INSERT INTO SELECT
:
Кроме того, мы удаляем 1000 случайных постов, повторно вставляя строки, но задавая значение колонки удалений равным 1. Снова, смоделировать это можно с помощью простого INSERT INTO SELECT
.
Результатом вышеперечисленных операций будет 16,000 строк, т.е. 10,000 + 5000 + 1000. Корректная сумма здесь составляет, на самом деле, 1000 строк меньше нашей первоначальной суммы, т.е. 10,000 - 1000 = 9000.
Ваши результаты будут варьироваться в зависимости от того, какие слияния произошли. Мы видим, что общее количество строк здесь отличается, так как у нас есть дублированные строки. Применение FINAL
к таблице дает корректный результат.
Производительность FINAL
Оператор FINAL
будет иметь накладные расходы на производительность запросов, несмотря на продолжающиеся улучшения. Это будет наиболее заметно, когда запросы не фильтруются по колонкам первичного ключа, что приводит к большему объему считываемых данных и увеличивает накладные расходы на дедупликацию. Если пользователи применяют фильтрацию по ключевым колонкам с использованием условия WHERE
, загружаемые данные и данные, передаваемые для дедупликации, будут уменьшены.
Если условие WHERE
не использует ключевую колонку, ClickHouse в настоящее время не использует оптимизацию PREWHERE
при применении FINAL
. Эта оптимизация направлена на сокращение количества строк, считываемых для неотфильтрованных колонок. Примеры имитации этого PREWHERE
и, следовательно, потенциального улучшения производительности можно найти здесь.
Использование партиций с ReplacingMergeTree
Слияние данных в ClickHouse происходит на уровне партиций. При использовании ReplacingMergeTree мы рекомендуем пользователям разбивать таблицу на партиции в соответствии с лучшими практиками, при условии, что пользователи могут гарантировать, что ключ партиционирования не изменяется для строки. Это гарантирует, что обновления, касающиеся одной и той же строки, будут направлены в одну и ту же партицию ClickHouse. Вы можете использовать тот же ключ партиционирования, что и в Postgres, при условии соблюдения описанных здесь лучших практик.
При условии, что это так, пользователи могут использовать настройку do_not_merge_across_partitions_select_final=1
для улучшения производительности запроса FINAL
. Эта настройка позволяет партициям объединяться и обрабатываться независимо при использовании FINAL
.
Рассмотрим таблицу постов, где мы не используем партиционирование:
Чтобы гарантировать, что FINAL
требуется выполнить какую-либо работу, мы обновляем 1 миллион строк - увеличивая их значение AnswerCount
путем вставки дубликатов.
Вычесляя сумму ответов за каждый год с FINAL
:
Повторяя эти самые шаги для таблицы, разбитой по годам, и повторяя вышеуказанный запрос с do_not_merge_across_partitions_select_final=1
.
Как видно, партиционирование значительно улучшило производительность запроса в данном случае, позволяя процессу дедупликации происходить на уровне партиции параллельно.
Учет поведения слияния
Механизм выбора слияний ClickHouse выходит за рамки простого слияния частей. Ниже мы рассматриваем это поведение в контексте ReplacingMergeTree, включая параметры конфигурации для включения более агрессивного слияния старых данных и соображения для более крупных частей.
Логика выбора слияния
В то время как слияние направлено на минимизацию числа частей, оно также балансирует эту цель с затратами на усиление записи. Следовательно, некоторые диапазоны частей исключаются из слияния, если это приведет к чрезмерному усилению записи на основе внутренних расчетов. Это поведение помогает предотвратить ненужное использование ресурсов и продлевает срок службы компонентов хранения.
Поведение слияния на крупных частях
Движок ReplacingMergeTree в ClickHouse оптимизирован для управления дублирующимися строками путем слияния частей данных, храня только последнюю версию каждой строки на основе заданного уникального ключа. Однако, когда объединенная часть достигает порога max_bytes_to_merge_at_max_space_in_pool, она больше не будет выбрана для дальнейшего слияния, даже если min_age_to_force_merge_seconds установлен. В результате автоматические слияния больше не могут полагаться на удаление дубликатов, которые могут накапливаться с продолжающимся добавлением данных.
Чтобы решить эту проблему, пользователи могут инициировать OPTIMIZE FINAL
для ручного слияния частей и удаления дубликатов. В отличие от автоматических слияний, OPTIMIZE FINAL
обходит порог max_bytes_to_merge_at_max_space_in_pool, сливая части исключительно на основе доступных ресурсов, особенно дискового пространства, до тех пор, пока в каждой партиции не останется одна часть. Однако этот подход может потреблять много памяти на больших таблицах и может потребовать повторного выполнения по мере добавления новых данных.
Для более устойчивого решения, поддерживающего производительность, рекомендуется партиционировать таблицу. Это может помочь предотвратить части данных от достижения максимального размера слияния и сократить необходимость в постоянной ручной оптимизации.
Партиционирование и слияние между партициями
Как обсуждалось в разделе Использование партиций с ReplacingMergeTree, мы рекомендуем партиционирование таблиц в качестве лучшей практики. Партиционирование изолирует данные для более эффективного слияния и избегает слияния между партициями, особенно во время выполнения запросов. Это поведение улучшено в версиях от 23.12 и далее: если ключ партиционирования является префиксом ключа сортировки, слияние между партициями не выполняется во время выполнения запросов, что приводит к более высокой производительности запросов.
Настройка слияний для лучшей производительности запросов
По умолчанию min_age_to_force_merge_seconds
и min_age_to_force_merge_on_partition_only
установлены в 0 и false соответственно, что отключает эти функции. В этой конфигурации ClickHouse будет применять стандартное поведение слияния без принудительного слияния на основе возраста партиции.
Если указано значение для min_age_to_force_merge_seconds
, ClickHouse будет игнорировать нормальные эвристики слияния для частей старше указанного периода. Хотя это обычно эффективно только в том случае, если целью является минимизация общего количества частей, это может улучшить производительность запросов в ReplacingMergeTree, сократив количество частей, требующих слияния во время выполнения запроса.
Это поведение можно дополнительно настроить, установив min_age_to_force_merge_on_partition_only=true
, что требует, чтобы все части в партиции были старше min_age_to_force_merge_seconds
для агрессивного слияния. Эта конфигурация позволяет старым партициям сжиматься до одной части с течением времени, что консолидируют данные и поддерживает производительность запросов.
Рекомендуемые настройки
Настройка поведения слияний является сложной операцией. Мы рекомендуем проконсультироваться с поддержкой ClickHouse перед включением этих настроек в производственных нагрузках.
В большинстве случаев предпочтительно установить min_age_to_force_merge_seconds
на низкое значение - значительно меньше времени партиции. Это минимизирует количество частей и предотвращает ненужные слияния во время выполнения запроса с оператором FINAL
.
Например, рассмотрим месячное партиционирование, которое уже было слито в одну часть. Если небольшая случайная вставка создаст новую часть в этой партиции, производительность запроса может пострадать, так как ClickHouse должен считывать несколько частей, пока завершится слияние. Установка min_age_to_force_merge_seconds
может гарантировать, что эти части слиты агрессивно, предотвращая ухудшение производительности запросов.