Проблема
ClickHouse Cloud не поддерживает транзакции из нескольких операторов в традиционном для RDBMS смысле. Это создает две распространенные проблемы:
- Атомарность в пределах одной таблицы для пакетной загрузки: распространенный подход — сначала выполнять вставку во временные частичные ключи, затем копировать записи в фактический ключ и удалять временные записи. Такой подход работает плохо — особенно на этапе удаления, который может занимать более 90% общего времени операции.
- Согласованность между несколькими таблицами: когда конвейер успешно загружает таблицу A, но завершается с ошибкой на таблице B, таблица A уже зафиксирована, и откатить изменения нельзя. Аналитики, выполняющие запросы к обеим таблицам, видят рассинхронизированные данные.
Предпосылки
ClickHouse гарантирует атомарность на уровне одной вставки в одну партицию: если INSERT выполняется успешно, все строки в этом блоке становятся видимыми; если завершается ошибкой — не видна ни одна. Однако встроенного механизма, позволяющего атомарно зафиксировать данные сразу в нескольких вставках или таблицах, нет.
Команды управления партициями (MOVE PARTITION TO TABLE, REPLACE PARTITION, ATTACH PARTITION FROM) работают на уровне метаданных, если исходная и целевая таблицы используют одну и ту же политику хранения.
Это означает, что они выполняются практически мгновенно независимо от объёма данных, поэтому они отлично подходят в качестве основы для шаблонов atomic-swap.
Рекомендуемое решение
Вместо того чтобы выполнять вставку напрямую в продукционные таблицы и пытаться устранять последствия сбоев, используйте выделенные staging-таблицы в качестве зоны загрузки. После проверки данных используйте операции на уровне партиций, чтобы атомарно перенести данные в продакшн.
Пошаговое руководство по обеспечению атомарности в пределах одной таблицы
Создайте staging-таблицу
Используйте ту же схему, ключ партиционирования, ORDER BY и политику хранения, что и у продукционной таблицы.
CREATE TABLE my_table_staging AS my_table_prod;Вставьте данные в staging-таблицу
Выполняйте вставку в staging-таблицу, а не в продукционную таблицу.
INSERT INTO my_table_staging SELECT ... FROM source;Если вставка завершилась ошибкой, очистите таблицу и повторите попытку
Очистите staging-таблицу и снова запустите загрузку. Данные в продакшн при этом не затрагиваются.
TRUNCATE TABLE my_table_staging;Если вставка прошла успешно, переместите партиции в продакшн
Чтобы переместить данные в продакшн и удалить их из staging-таблицы, используйте MOVE PARTITION.
ALTER TABLE my_table_staging MOVE PARTITION <partition_expr> TO TABLE my_table_prod;Либо скопируйте данные в существующую партицию в продакшн с помощью ATTACH PARTITION.
ALTER TABLE my_table_prod ATTACH PARTITION tuple() FROM my_table_staging;Обе операции выполняются на уровне метаданных в рамках одной политики хранения и завершаются почти мгновенно.
Очистите staging-таблицу
После перемещения всех партиций очистите staging-таблицу, чтобы она оставалась пустой для следующей загрузки.
TRUNCATE TABLE my_table_staging;Согласованность между несколькими таблицами
Тот же подход решает проблему конвейеров, в которых две или более таблиц должны быть полностью загружены, прежде чем хоть одна из них станет видимой для аналитиков. Загружайте данные каждой таблицы в её собственную staging-таблицу и проверяйте их все, а затем одновременно выполняйте перемещения партиций. Поскольку каждое такое перемещение — почти мгновенная операция с метаданными, период, в течение которого данные в таблицах могут быть несогласованными, сокращается с длительности полной загрузки до времени, нужного для переключения партиций.
Требования и ограничения
Для MOVE PARTITION TO TABLE, REPLACE PARTITION и ATTACH PARTITION FROM исходная и целевая таблицы должны иметь:
- Одинаковую структуру столбцов
- Одинаковый ключ партиционирования, ключ
ORDER BYи первичный ключ - Одинаковую политику хранения
- Целевая таблица должна содержать все индексы и проекции из исходной таблицы
Пример
Вы можете интерактивно выполнить приведённый ниже пример с помощью fiddle:
CREATE TABLE prod
(
uid Int16,
name String,
age Int16
)
ENGINE=MergeTree
ORDER BY ();
CREATE TABLE staging
(
uid Int16,
name String,
age Int16
)
ENGINE=MergeTree
ORDER BY ();
-- Начальные данные
INSERT INTO prod VALUES (123, 'John', 33);
INSERT INTO prod VALUES (456, 'Ksenia', 48);
-- Загрузка данных
INSERT INTO staging VALUES (8811, 'Alice', 50);
INSERT INTO staging VALUES (8812, 'Bob', 23);
-- Проверка импорта
SELECT 'Staging count:', COUNT() FROM staging;
-- Перемещение партиции
ALTER TABLE staging MOVE PARTITION tuple() TO TABLE prod; -- атомарная операция
-- Проверка данных
SELECT 'Prod count:', COUNT() FROM prod;
SELECT * FROM prod;Справка
- Управление партициями и частями
- Подробнее об этой стратегии см. в блоге Ускорение загрузки больших объёмов данных в ClickHouse — часть 3: как сделать загрузку больших объёмов данных отказоустойчивой.