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

Обзор

При работе с обновлениями аналитические и транзакционные базы данных значительно различаются из-за различий в их базовых принципах проектирования и целевых сценариях использования. Например, PostgreSQL — это ориентированная на строки реляционная база данных с поддержкой ACID, обеспечивающая надёжные и транзакционные операции обновления и удаления, гарантирующие согласованность и целостность данных с помощью таких механизмов, как Multi-Version Concurrency Control (MVCC). Это позволяет безопасно и предсказуемо вносить изменения даже в средах с высокой конкуррентностью.

Напротив, ClickHouse — это колонко-ориентированная база данных, оптимизированная для аналитических нагрузок с преобладанием чтения и высокопроизводительных операций добавления (append-only). Хотя она изначально поддерживает обновления и удаления «на месте», их необходимо использовать осторожно, чтобы избежать высокой нагрузки на I/O. В качестве альтернативы таблицы можно реорганизовать так, чтобы операции удаления и обновления превращались в операции добавления, обрабатываемые асинхронно и/или во время чтения, что отражает ориентацию на высокопроизводительную ингестию данных и эффективное выполнение запросов, а не на изменение данных в режиме реального времени.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.

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

Мутации

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

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

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

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

Примечание

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

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

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

Легковесные обновления используют "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 может вызывать тайм-ауты сессий, затрагивающие другие таблицы.

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

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

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

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

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

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