Перейти к основному содержимому
Перейти к основному содержимому

Обновления в ClickHouse

Когда речь идёт об обновлениях, аналитические и транзакционные базы данных используют разные подходы, обусловленные базовыми принципами их архитектуры и целевыми сценариями использования. ClickHouse — это столбцовая база данных, оптимизированная для аналитических нагрузок с преобладанием чтения и операций только добавления с высокой пропускной способностью. На практике таблицы часто перестраивают так, чтобы преобразовать удаления и обновления в операции добавления, которые обрабатываются асинхронно и/или во время чтения, используя сильные стороны ClickHouse для высокопроизводительной ингестии данных. ClickHouse также поддерживает полноценные операции обновления и удаления.

Это руководство даёт обзор доступных в ClickHouse методов обновления и помогает выбрать подходящую стратегию обновления для вашей нагрузки.

Выбор стратегии обновления

Существует два базовых подхода к обновлению данных в ClickHouse:

  1. Использование специализированных движков таблиц, которые реализуют обновления через вставки
  2. Использование декларативных обновлений, таких как команды UPDATE ... SET или ALTER TABLE ... UPDATE

В каждой из двух категорий выше существует несколько способов обновления данных. У каждого подхода есть свои преимущества и характеристики производительности, и вам следует выбрать соответствующий метод в зависимости от вашей модели данных и объёма данных, который вы планируете обновлять.

Когда использовать специализированные движки таблиц

Специализированные движки таблиц — лучший выбор, когда у вас большие объемы обновлений, частые изменения на уровне строк или необходимо обрабатывать непрерывный поток событий обновления и удаления.

Чаще всего вы будете сталкиваться со следующими движками:

ДвижокСинтаксисКогда использовать
ReplacingMergeTreeENGINE = ReplacingMergeTreeИспользуйте при обновлении больших объемов данных. Этот движок таблицы оптимизирован для дедупликации данных при слияниях.
CoalescingMergeTreeENGINE = CoalescingMergeTreeИспользуйте, когда данные приходят фрагментами и вам требуется коалесцирование на уровне столбцов, а не полная замена строк.
CollapsingMergeTreeENGINE = CollapsingMergeTree(Sign)Используйте при частых обновлениях отдельных строк или в сценариях, когда необходимо поддерживать актуальное состояние объектов, изменяющихся со временем. Например, отслеживание активности пользователей или статистики по статьям.

Поскольку движки таблиц семейства MergeTree выполняют слияние частей данных в фоновом режиме, они обеспечивают согласованность в конечном счёте, и для обеспечения корректной дедупликации данных в этот промежуточный период при выполнении запросов к таблице необходимо использовать ключевое слово FINAL. Существуют и другие типы движков, но эти используются чаще всего.

Когда использовать декларативные обновления

Декларативные операторы UPDATE могут быть более простыми для несложных операций обновления без необходимости управления логикой дедупликации, но в целом они лучше подходят для обновления меньшего количества строк и при меньшей частоте, чем при использовании специализированных движков.

МетодСинтаксисКогда использовать
Легковесные обновленияUPDATE [table] SET ... WHEREИспользуйте это в большинстве сценариев, особенно при выполнении частых небольших UPDATE (до ~10% таблицы) в рамках вашего приложения или рабочих процессов. Например, пользователь хочет удалить свою историю событий, а сами события распределены по мультитенантной таблице со множеством пользователей. Создаёт патч-части для немедленной видимости без перезаписи целых столбцов. Добавляет накладные расходы к запросам SELECT, но обеспечивает предсказуемую задержку.
Мутация обновленияALTER TABLE [table] UPDATEИспользуйте это при выполнении более масштабных операций управления данными, особенно когда обновление соответствует схеме партиционирования таблицы. Например, если нужно обновить столбец во всех строках за месяц в таблице, разбитой на партиции по месяцам.

Обновления данных с использованием специализированных движков таблиц

ReplacingMergeTree

ReplacingMergeTree удаляет дубликаты строк с одинаковым ключом сортировки во время фоновых слияний, сохраняя только самую новую версию.

CREATE TABLE posts
(
    Id UInt32,
    Title String,
    ViewCount UInt32,
    Version UInt32
)
ENGINE = ReplacingMergeTree(Version)
ORDER BY Id

Этот движок идеально подходит для высокочастотных обновлений отдельных строк, где обновления идентифицируются по стабильному ключу. Бенчмарки показывают, что он может быть до 4 700 раз быстрее, чем мутации, при обновлениях одной строки.

Чтобы обновить строку, просто вставьте новую версию с теми же значениями ключа сортировки и более высоким номером версии. Старые версии удаляются во время фоновых слияний. Поскольку дедупликация выполняется не сразу (она происходит только во время слияний), следует использовать модификатор FINAL или эквивалентную логику запроса, чтобы получить корректные, дедуплицированные результаты. Модификатор FINAL добавляет накладные расходы на выполнение запроса в диапазоне от 21 до 550% в зависимости от данных.

ReplacingMergeTree не может изменять значения ключа сортировки. Он также поддерживает столбец Deleted для логического удаления.

Подробнее: Руководство по ReplacingMergeTree | Справочник по ReplacingMergeTree

CoalescingMergeTree

CoalescingMergeTree консолидирует разреженные записи, сохраняя последнее ненулевое значение для каждого столбца при слияниях. Это позволяет выполнять операции upsert на уровне столбцов, а не полную замену всей строки.

CREATE TABLE electric_vehicle_state
(
    vin String, -- vehicle identification number
    last_update DateTime64 Materialized now64(), -- optional (used with argMax)
    battery_level Nullable(UInt8), -- in %
    lat Nullable(Float64), -- latitude (°)
    lon Nullable(Float64), -- longitude (°)
    firmware_version Nullable(String),
    cabin_temperature Nullable(Float32), -- in °C
    speed_kmh Nullable(Float32) -- from sensor
)
ENGINE = CoalescingMergeTree
ORDER BY vin;

Этот движок предназначен для сценариев, когда данные поступают фрагментами из нескольких источников или когда разные столбцы заполняются в разное время. Типичные варианты использования включают IoT‑телеметрию от фрагментированных подсистем, обогащение пользовательских профилей и ETL‑конвейеры с отложенными измерениями.

При слиянии строк с одинаковым сортировочным ключом CoalescingMergeTree сохраняет последнее ненулевое (не NULL) значение для каждого столбца вместо замены всей строки. Неключевые столбцы должны быть типа Nullable, чтобы это работало как задумано. Как и в ReplacingMergeTree, используйте FINAL для корректных, коалесцированных результатов.

Этот движок доступен начиная с ClickHouse 25.6.

Подробнее: CoalescingMergeTree

CollapsingMergeTree

Исходя из идеи, что операции обновления дорогостоящи, тогда как вставки можно использовать для реализации обновлений, CollapsingMergeTree использует столбец Sign, чтобы указать ClickHouse, как обрабатывать строки во время слияний. Если для столбца Sign вставлено значение -1, строка будет свернута (удалена) при объединении с соответствующей строкой с +1. Строки для обновления определяются на основе ключа сортировки, указанного в предложении ORDER BY при создании таблицы.

CREATE TABLE user_activity
(
    UserID UInt64,
    PageViews UInt8,
    Duration UInt8,
    Sign Int8
)
ENGINE = CollapsingMergeTree(Sign)
ORDER BY UserID

-- Initial state
INSERT INTO user_activity VALUES (4324182021466249494, 5, 146, 1)

-- Cancel old row and insert new state
INSERT INTO user_activity VALUES (4324182021466249494, 5, 146, -1)
INSERT INTO user_activity VALUES (4324182021466249494, 6, 185, 1)

-- Query with proper aggregation
SELECT
    UserID,
    sum(PageViews * Sign) AS PageViews,
    sum(Duration * Sign) AS Duration
FROM user_activity
GROUP BY UserID
HAVING sum(Sign) > 0

┌──────────────UserID─┬─PageViews─┬─Duration─┐
│ 4324182021466249494 │         6 │      185 │
└─────────────────────┴───────────┴──────────┘

В отличие от ReplacingMergeTree, CollapsingMergeTree позволяет изменять значения ключа сортировки. Он хорошо подходит для обратимых операций с семантикой отмены, таких как финансовые транзакции или отслеживание состояния игры.

Примечание

Подход к обновлению, описанный выше, требует, чтобы ваше приложение поддерживало состояние на стороне клиента, чтобы вставлять строку отмены. Хотя это наиболее эффективно с точки зрения ClickHouse, с этим может быть сложно работать в крупном масштабе. Запросы также должны выполнять агрегацию с умножением на знак, чтобы получать корректные результаты.

Подробнее: CollapsingMergeTree

Декларативные обновления

Эти методы работают с таблицами на движках семейства MergeTree.

методсинтаксисBest forTrade-offs
МутацииALTER TABLE ... UPDATEРедкие массовые обновления; особенно хорошо подходят, когда обновление согласуется с партиционированием таблицы.Высокая нагрузка на ввод-вывод; переписываются столбцы
Легковесные обновленияUPDATE ... SET ... WHEREНебольшие обновления (~0,1–10% строк); частые обновления, требующие высокой производительностиУвеличивает накладные расходы на SELECT; патч-части учитываются в лимитах

Мутации

Мутации (ALTER TABLE ... UPDATE) перезаписывают все части, содержащие строки, удовлетворяющие условию WHERE.

ALTER TABLE posts UPDATE AnswerCount = AnswerCount + 1 WHERE AnswerCount = 0

Мутации создают значительную нагрузку на I/O, так как переписывают все части, которые соответствуют выражению WHERE. В этом процессе нет атомарности. Части заменяются на Мутации, как только они готовы, и запрос SELECT, который начинает выполняться во время Мутации, будет видеть данные как из уже изменённых частей, так и из тех, которые ещё не были изменены. Вы можете отслеживать ход выполнения через таблицу system.mutations.

Примечание

Мутации создают значительную нагрузку на I/O и должны применяться ограниченно, так как они могут влиять на производительность запросов SELECT в кластере. Если Мутации ставятся в очередь быстрее, чем успевают обрабатываться, производительность запросов будет ухудшаться. Отслеживайте очередь через system.mutations.

Подробнее: ALTER TABLE UPDATE

Мутации на лету

При использовании мутаций через ALTER TABLE ... UPDATE может потребоваться подождать, пока они будут применены фоновым процессом, прежде чем измененные значения начнут отражаться в результатах запросов. ClickHouse позволяет изменить это поведение с помощью "мутаций на лету". Когда мутации на лету включены, обновленные строки сразу помечаются как обновленные, и последующие запросы SELECT автоматически возвращают измененные значения.

Мутации на лету можно включить для таблиц семейства MergeTree, включив настройку уровня запроса apply_mutations_on_fly.

SET apply_mutations_on_fly = 1;
Пример

Создадим таблицу и выполним несколько мутаций:

CREATE TABLE test_on_fly_mutations (id UInt64, v String)
ENGINE = MergeTree ORDER BY id;

-- Отключим фоновую материализацию мутаций, чтобы показать
-- поведение по умолчанию, когда мутации на лету не включены
SYSTEM STOP MERGES test_on_fly_mutations;
SET mutations_sync = 0;

-- Вставим несколько строк в новую таблицу
INSERT INTO test_on_fly_mutations VALUES (1, 'a'), (2, 'b'), (3, 'c');

-- Обновим значения строк
ALTER TABLE test_on_fly_mutations UPDATE v = 'd' WHERE id = 1;
ALTER TABLE test_on_fly_mutations DELETE WHERE v = 'd';
ALTER TABLE test_on_fly_mutations UPDATE v = 'e' WHERE id = 2;
ALTER TABLE test_on_fly_mutations DELETE WHERE v = 'e';

Проверим результат обновлений с помощью запроса SELECT:

-- Явно отключим мутации на лету
SET apply_mutations_on_fly = 0;

SELECT id, v FROM test_on_fly_mutations ORDER BY id;

Обратите внимание: на момент выполнения запроса значения строк в таблице еще не обновлены:

┌─id─┬─v─┐
│  1 │ a │
│  2 │ b │
│  3 │ c │
└────┴───┘

Теперь посмотрим, что произойдет, если включить мутации на лету:

-- Включим мутации на лету
SET apply_mutations_on_fly = 1;

SELECT id, v FROM test_on_fly_mutations ORDER BY id;

Теперь запрос SELECT сразу возвращает правильный результат, без ожидания применения мутаций:

┌─id─┬─v─┐
│  3 │ c │
└────┴───┘
Влияние на производительность

Когда включены мутации на лету, мутации материализуются не сразу, а применяются только при выполнении запросов SELECT. Однако обратите внимание, что мутации по-прежнему асинхронно материализуются в фоновом режиме, а это ресурсоемкий процесс.

Если количество отправленных мутаций в течение некоторого интервала времени постоянно превышает количество мутаций, обрабатываемых в фоновом режиме, очередь нематериализованных мутаций, которые необходимо применить, будет продолжать расти. В конечном итоге это приведет к ухудшению производительности запросов SELECT.

Мы рекомендуем включать настройку apply_mutations_on_fly вместе с другими настройками уровня MergeTree, такими как number_of_mutations_to_throw и number_of_mutations_to_delay, чтобы ограничить неограниченный рост числа нематериализованных мутаций.

Поддержка подзапросов и недетерминированных функций

Мутации на лету ограниченно поддерживают подзапросы и недетерминированные функции. Поддерживаются только скалярные подзапросы с результатом разумного размера (это определяется настройкой mutations_max_literal_size_to_replace). Также поддерживаются только константные недетерминированные функции (например, функция now()).

Эти особенности управляются следующими настройками:

НастройкаОписаниеПо умолчанию
mutations_execute_nondeterministic_on_initiatorЕсли установлено значение true, недетерминированные функции выполняются на реплике-инициаторе и заменяются литералами в запросах UPDATE и DELETE.false
mutations_execute_subqueries_on_initiatorЕсли установлено значение true, скалярные подзапросы выполняются на реплике-инициаторе и заменяются литералами в запросах UPDATE и DELETE.false
mutations_max_literal_size_to_replaceМаксимальный размер сериализованных литералов в байтах, которыми заменяются значения в запросах UPDATE и DELETE.16384 (16 KiB)

Легковесные обновления

Легковесные обновления используют "patch parts" — специальные части данных, которые содержат только обновлённые столбцы и строки, — вместо перезаписи целых столбцов, как при традиционных Мутациях.

UPDATE posts SET AnswerCount = AnswerCount + 1 WHERE Id = 404346

Этот подход использует стандартный синтаксис UPDATE и создает части‑патчи немедленно, не дожидаясь слияний. Обновленные значения сразу видны в запросах SELECT за счет применения патчей, но физически материализуются только во время последующих слияний. Это делает легковесное обновление оптимальным для изменения небольшой доли строк (до ~10% таблицы) с предсказуемой задержкой. Бенчмарки показывают, что такие операции могут быть до 23 раз быстрее, чем Мутации.

Компромисс состоит в том, что запросы SELECT создают дополнительные накладные расходы при применении патчей, а части‑патчи учитываются в лимитах частей. При превышении порога ~10% накладные расходы механизма patch‑on‑read растут пропорционально, что делает синхронные Мутации более эффективными для крупных обновлений.

Подробнее: Lightweight UPDATE

Мутации на лету

Мутации на лету предоставляют механизм обновления строк таким образом, что последующие запросы SELECT автоматически возвращают изменённые значения без ожидания завершения фоновой обработки. Это эффективно устраняет ограничение обычных мутаций, связанное с атомарностью.

SET apply_mutations_on_fly = 1;

SELECT ViewCount FROM posts WHERE Id = 404346

┌─ViewCount─┐
│     26762 │
└───────────┘

-- Increment the count
ALTER TABLE posts UPDATE ViewCount = ViewCount + 1 WHERE Id = 404346

-- The updated value is immediately visible
SELECT ViewCount FROM posts WHERE Id = 404346

┌─ViewCount─┐
│     26763 │
└───────────┘

И для мутации, и для последующих запросов SELECT необходимо включить настройку apply_mutations_on_fly = 1. Условия мутаций хранятся в ClickHouse Keeper, который хранит всё в памяти, и применяются на лету во время выполнения запросов.

Обратите внимание, что для обновления данных по‑прежнему используется мутация — она просто не материализуется сразу. Мутация всё равно будет применена в фоновом режиме как асинхронный процесс и несёт такую же серьёзную нагрузку, как и обычная мутация. Выражения, которые можно использовать с этой операцией, также ограничены (см. подробности).

Примечание

Мутации на лету следует использовать только для небольшого числа операций — максимум нескольких десятков. Keeper хранит условия в памяти, поэтому чрезмерное использование влияет на стабильность кластера. Сильная нагрузка на Keeper может вызывать тайм-ауты сессий, затрагивающие другие таблицы.

Подробнее: Мутации на лету

Итоги сравнения

В следующей таблице обобщены накладные расходы на выполнение запросов на основе бенчмарков. Мутации приняты за базовый уровень, поскольку запросы выполняются на полной скорости после завершения мутации и физической перезаписи данных.

МетодЗамедление запросовНакладные расходы по памятиПримечания
МутацииБазовый уровеньБазовый уровеньПолная скорость после завершения; данные физически перезаписываются
Мутации на летуПеременноеПеременноеМгновенная видимость; производительность ухудшается, если накапливается много обновлений
Легковесные обновления7–18% (в среднем ~12%)+20–210%Наиболее эффективны для запросов; лучше всего подходят для обновления ≤10% таблицы
ReplacingMergeTree + FINAL21–550% (в среднем ~280%)В 20–200 раз больше базового уровняНеобходимо читать все версии строк; самые высокие накладные расходы на запросы
CoalescingMergeTree + FINALАналогично ReplacingMergeTreeАналогично ReplacingMergeTreeОбъединение на уровне столбцов добавляет сопоставимые накладные расходы
CollapsingMergeTreeЗависит от агрегацииЗависит от агрегацииНакладные расходы зависят от сложности запроса

Дополнительные ресурсы

Если вам интересен подробный разбор того, как со временем эволюционировали обновления в ClickHouse, а также анализ бенчмарков, см. следующие материалы: