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
.
Рассмотрим вышеуказанную таблицу постов. Мы можем использовать обычный метод загрузки этого набора данных, но указать колонку удаления и версию в дополнение к значениям 0. Для примера мы загружаем только 10000 строк.
Давайте подтвердим количество строк:
Теперь мы обновляем статистику постов и ответов. Вместо обновления этих значений мы вставляем новые копии 5000 строк и увеличиваем их номер версии на 1 (это означает, что 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 может гарантировать, что эти части будут объединены агрессивно, предотвращая ухудшение производительности запросов.