ReplacingMergeTree
В то время как транзакционные базы данных оптимизированы для обновлений и удалений транзакционного характера, OLAP базы данных предлагают сниженные гарантии для таких операций. Вместо этого они оптимизируют для неизменяемых данных, вставленных пакетами, что значительно ускоряет аналитические запросы. Хотя ClickHouse предлагает операции обновления через мутации, а также легковесный способ удаления строк, его столбцовая структура подразумевает, что эти операции должны выполняться с осторожностью, как описано выше. Эти операции обрабатываются асинхронно, обрабатываются с помощью одного потока и требуют (в случае обновлений) переписывания данных на диск. Поэтому их не следует использовать для большого количества небольших изменений. Для обработки потока обновлений и удалений строк, избегая вышеуказанных шаблонов использования, мы можем использовать движок таблиц ClickHouse ReplacingMergeTree.
Автоматические обновления вставленных строк
Движок таблицы ReplacingMergeTree позволяет применять операции обновления к строкам, не используя неэффективные операторы ALTER
или DELETE
, предоставляя возможность пользователям вставлять несколько копий одной и той же строки и обозначать одну как последнюю версию. Процесс в фоновом режиме, в свою очередь, асинхронно удаляет старые версии одной и той же строки, эффективно имитируя операцию обновления с использованием неизменяемых вставок. Это зависит от способности движка таблицы идентифицировать дублирующиеся строки. Это достигается с помощью условия ORDER BY
, чтобы определить уникальность, т.е. если две строки имеют одинаковые значения для столбцов, указанных в ORDER BY
, они считаются дубликатами. Столбец 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
.
Рассмотрим таблицу постов выше. Мы можем использовать обычный метод загрузки этого набора данных, но дополнить его столбцами Deleted
и Version
с значениями 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 может гарантировать, что эти части будут объединены агрессивно, предотвращая ухудшение производительности запроса.