Автоматические upsert-операции для вставленных строк
ALTER или DELETE: для этого можно вставить несколько копий одной и той же строки и пометить одну из них как самую новую версию. Затем фоновый процесс асинхронно удаляет более старые версии одной и той же строки, эффективно имитируя обновление за счет неизменяемых вставок.
Это основано на способности движка таблицы выявлять дублирующиеся строки. Для определения уникальности используется предложение ORDER BY: если две строки имеют одинаковые значения в столбцах, указанных в ORDER BY, они считаются дубликатами. Столбец version, задаваемый при определении таблицы, позволяет сохранить самую новую версию строки, когда две строки распознаются как дубликаты, то есть сохраняется строка с наибольшим значением версии.
Мы проиллюстрируем этот процесс на примере ниже. Здесь строки однозначно определяются столбцом A (ORDER BY для таблицы). Предположим, что эти строки были вставлены двумя батчами, в результате чего на диске сформировались две части данных. Позже в ходе асинхронного фонового процесса эти части объединяются.
ReplacingMergeTree также позволяет указать столбец deleted. Он может содержать только 0 или 1: значение 1 указывает, что строка (и её дубликаты) была удалена, а 0 используется в остальных случаях. Примечание: удалённые строки не удаляются во время слияния.
Во время этого процесса при слиянии частей происходит следующее:
- Для строки, определяемой значением 1 в столбце A, есть как строка обновления с версией 2, так и строка удаления с версией 3 (и значением 1 в столбце deleted). Поэтому сохраняется последняя строка, помеченная как удалённая.
- Для строки, определяемой значением 2 в столбце A, есть две строки обновления. Сохраняется последняя строка со значением 6 в столбце price.
- Для строки, определяемой значением 3 в столбце A, есть строка с версией 1 и строка удаления с версией 2. Сохраняется строка удаления.
Обратите внимание, что удалённые строки никогда не удаляются. Их можно принудительно удалить с помощью
OPTIMIZE table FINAL CLEANUP. Для этого требуется экспериментальная настройка allow_experimental_replacing_merge_with_cleanup=1. Это следует делать только при следующих условиях:
- Вы уверены, что после выполнения операции не будут вставлены строки со старыми версиями (для строк, удаляемых при очистке). Если такие строки будут вставлены, они будут ошибочно сохранены, поскольку удалённые строки к тому моменту уже не будут присутствовать.
- Перед выполнением очистки убедитесь, что все реплики синхронизированы. Этого можно добиться с помощью команды:
Обрабатывать удаления с помощью ReplacingMergeTree рекомендуется только для таблиц с небольшим или умеренным числом удалений (менее 10%), если только не предусмотрены периоды для очистки при соблюдении указанных выше условий.
Совет: также можно выполнить OPTIMIZE FINAL CLEANUP для отдельных партиций, в которые больше не вносятся изменения.
Выбор первичного ключа / ключа дедупликации
ORDER BY должны однозначно идентифицировать строку при всех изменениях. Поэтому при миграции из транзакционной базы данных, такой как Postgres, исходный primary key Postgres следует включать в выражение ORDER BY в ClickHouse.
Пользователи ClickHouse хорошо знают, как выбирать столбцы для ORDER BY в своих таблицах, чтобы оптимизировать производительность запросов. Как правило, эти столбцы следует выбирать на основе ваших часто выполняемых запросов и располагать в порядке возрастания мощности. Важно, что ReplacingMergeTree накладывает дополнительное ограничение: эти столбцы должны быть неизменяемыми, то есть при репликации из Postgres добавлять в это выражение следует только те столбцы, которые не меняются в исходных данных Postgres. Хотя другие столбцы могут изменяться, эти должны оставаться постоянными для однозначной идентификации строки.
Для аналитических рабочих нагрузок primary key Postgres обычно мало полезен, поскольку точечный lookup строк требуется редко. Учитывая, что мы рекомендуем располагать столбцы в порядке возрастания мощности, а также то, что совпадения по столбцам, указанным раньше в ORDER BY, обычно обрабатываются быстрее, primary key Postgres следует добавлять в конец ORDER BY (если только он не представляет аналитической ценности). Если в Postgres primary key состоит из нескольких столбцов, их следует добавить в ORDER BY с учетом мощности и вероятной пользы для запросов. Вы также можете создать уникальный primary key, объединив значения в столбце MATERIALIZED.
Рассмотрим таблицу Posts из набора данных Stack Overflow.
ORDER BY (PostTypeId, toDate(CreationDate), CreationDate, Id). Столбец Id, уникальный для каждого поста, обеспечивает возможность дедупликации строк. В схему также добавляются столбцы Version и Deleted, как требуется.
Запросы к ReplacingMergeTree
ORDER BY как уникальный идентификатор, и либо сохраняет только версию с наибольшим номером, либо удаляет все дубликаты, если последняя версия помечена как удалённая. Однако это обеспечивает лишь согласованность в конечном счёте — нет гарантии, что строки будут дедуплицированы, поэтому полагаться на это не следует.
Используйте FINAL для чтения дедуплицированных данныхПоскольку дедупликация происходит только во время фоновых слияний, обычный
SELECT всё ещё может возвращать дублирующиеся или удалённые строки. Чтобы получать корректные результаты на этапе выполнения запроса, используйте модификатор FINAL, который завершает дедупликацию и удаление удалённых строк в ходе выполнения запроса.INSERT INTO SELECT:
INSERT INTO SELECT.
FINAL к таблице дает правильный результат.
Производительность FINAL
FINAL действительно вносит небольшие накладные расходы в производительность запросов.
Это особенно заметно, когда запросы не фильтруются по столбцам первичного ключа,
из-за чего приходится читать больше данных и возрастают затраты на дедупликацию. Если вы
фильтруете по столбцам ключа с помощью условия WHERE, объем данных, загружаемых и передаваемых для
дедупликации, уменьшится.
Если в условии WHERE не используется столбец ключа, ClickHouse в настоящее время не применяет оптимизацию PREWHERE при использовании FINAL. Эта оптимизация предназначена для сокращения числа читаемых строк для нефильтруемых столбцов. Примеры того, как эмулировать это поведение PREWHERE и тем самым потенциально повысить производительность, можно найти здесь.
Использование партиций с ReplacingMergeTree
do_not_merge_across_partitions_select_final=1, чтобы повысить производительность запросов с FINAL. Эта настройка приводит к тому, что при использовании FINAL партиции сливаются и обрабатываются независимо друг от друга.
Рассмотрим следующую таблицу posts, в которой партиционирование не используется:
FINAL действительно было что обрабатывать, мы обновляем 1 млн строк, увеличивая их AnswerCount за счёт вставки дублирующих строк.
FINAL:
do_not_merge_across_partitions_select_final=1.
Особенности поведения при слиянии
Логика выбора для слияния
Поведение слияния для больших частей
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 и выполняет слияние частей, исходя только из доступных ресурсов, прежде всего дискового пространства, пока в каждой партиции не останется по одной части. Однако на больших таблицах такой подход может требовать значительного объёма памяти, и его, возможно, придётся запускать повторно по мере добавления новых данных.
Более устойчивое решение, позволяющее сохранить производительность, — партиционировать таблицу. Это помогает не допускать, чтобы части данных достигали максимального размера для слияния, и снижает необходимость в постоянной ручной оптимизации.
Партиционирование и слияние между партициями
Настройка слияний для повышения производительности запросов
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. Такая конфигурация позволяет старым партициям со временем сливаться в одну часть, что консолидирует данные и помогает поддерживать производительность запросов.
Рекомендуемые настройки
min_age_to_force_merge_seconds низкое значение — значительно меньшее, чем период партиции. Это сводит к минимуму количество частей и предотвращает ненужное слияние на этапе выполнения запроса с оператором FINAL.
Например, рассмотрим месячную партицию, которая уже была объединена в одну часть. Если небольшая случайная вставка создает новую часть в этой партиции, производительность запросов может снизиться, поскольку ClickHouse должен читать несколько частей, пока не завершится слияние. Параметр min_age_to_force_merge_seconds позволяет обеспечить более агрессивное слияние этих частей, предотвращая снижение производительности запросов.