Delta Lake connector: transaction log, deletion vectors, change data feed
Мы разобрали два формата таблиц: Iceberg с деревом метаданных и снапшотами и Hive с метаданными в Metastore до уровня партиции. Третий формат lakehouse — Delta Lake, и это полноценный современный формат таблиц того же класса, что Iceberg. Delta решает те же задачи — атомарность, time travel, row-level DML, — но архитектурой метаданных отличается: вместо дерева снапшотов у него transaction log. Этот урок разбирает устройство Delta-таблицы, механику transaction log, deletion vectors и change data feed, и сравнивает Delta с Iceberg.
Transaction log: журнал изменений вместо дерева снапшотов
Delta-таблица, как и Iceberg, — это файлы данных в Parquet плюс слой метаданных в том же object storage. Но слой метаданных устроен иначе. Сердце Delta-таблицы — transaction log, журнал транзакций. Он лежит в поддиректории _delta_log/ внутри директории таблицы.
Transaction log — это упорядоченная последовательность пронумерованных JSON-файлов: 00000000000000000000.json, 00000000000000000001.json, и так далее. Каждый такой файл — одна транзакция, одно зафиксированное изменение таблицы. Содержимое файла — это не данные, а список действий (actions): «добавлен файл данных X», «удалён файл данных Y», «изменена схема», «изменены свойства таблицы».
Принципиальная идея: чтобы узнать текущее состояние Delta-таблицы, движок читает transaction log с начала и проигрывает все действия по порядку. Транзакция 5 добавила файлы A, B, C. Транзакция 9 удалила файл B, добавила D. Текущий набор файлов — A, C, D. Состояние таблицы — это результат свёртки всего журнала. Это модель event sourcing: состояние не хранится напрямую, оно выводится из последовательности событий.
Чтобы не перечитывать тысячи JSON-файлов на каждый запрос, Delta периодически создаёт checkpoint — файл в формате Parquet, который хранит уже свёрнутое состояние таблицы на определённую транзакцию. При чтении движок берёт последний checkpoint и доигрывает только JSON-файлы после него. Это та же идея, что checkpoint в журналах СУБД: не проигрывать журнал с самого начала времён.
Сравните с Iceberg. У Iceberg — дерево: metadata-файл указывает на снапшот, снапшот на manifest list, тот на манифесты. Каждый снапшот хранит полный список файлов своего состояния. У Delta — линейный журнал действий, и состояние выводится их свёрткой. Обе модели дают атомарность, time travel и метаданные уровня файла — но Iceberg хранит состояния, а Delta хранит переходы между ними.
Delta Lake: архитектура Transaction Log Выбор Table Format: Delta Lake vs Iceberg vs Hudi vs PaimonКак Delta обеспечивает атомарность
Атомарность коммита в Delta — это атомарное создание следующего пронумерованного JSON-файла в _delta_log/. Транзакция, стартовавшая от версии 8, готовит свои действия и пытается записать файл ...009.json. Если запись успешна — транзакция зафиксирована, версия таблицы стала 9.
Конкуренция здесь решается так же оптимистично, как в Iceberg. Если две транзакции стартовали от версии 8, обе хотят создать ...009.json. Создать файл с этим именем сможет только одна — вторая получит конфликт (имя занято), перечитает журнал, увидит чужую транзакцию 9 и повторит попытку, целясь уже в ...010.json. Это оптимистичная конкуренция через гонку за следующее имя файла в журнале.
# etc/catalog/delta.properties
connector.name=delta_lake
hive.metastore=thrift
hive.metastore.uri=thrift://hms:9083
fs.native-s3.enabled=true
s3.endpoint=http://minio:9000
s3.path-style-access=true
# Разрешить процедуру register_table
delta.register-table-procedure.enabled=true
Заметьте: Delta-коннектор тоже использует metastore (HMS или Glue) — но не для хранения списка файлов, а лишь как реестр «имя таблицы -> её директория». Сам список файлов и вся история — в _delta_log/, а не в metastore. Metastore для Delta — это указатель на таблицу, а transaction log — источник правды о её содержимом.
Конкурентная запись в Delta на S3 требует внимания. Атомарность зависит от того, что создать файл с уже существующим именем нельзя. Современные S3 это гарантируют через conditional writes, но в части конфигураций или при нескольких писателях используют свойство delta.enable-non-concurrent-writes — его включают, только когда гарантировано, что в таблицу одновременно не пишет несколько процессов. Включить его при реальной конкуренции — риск потери коммита.
Deletion vectors: удаление без переписывания файла
Delta — современный формат, и row-level DELETE и UPDATE он поддерживает. Базовый механизм: чтобы удалить строки из файла, Delta может переписать файл без них и записать в журнал действия «remove старый файл, add новый». Это работает, но дорого — переписывается весь файл ради удаления нескольких строк.
Deletion vectors — оптимизация этого механизма. Вместо переписывания файла данных Delta пишет рядом компактный битовый вектор: битмап позиций строк, помеченных удалёнными в этом файле. Файл данных не трогается. При чтении движок применяет deletion vector — пропускает строки, чьи позиции помечены в векторе.
Выигрыш виден на типичном сценарии. DELETE FROM accounts WHERE status = 'closed' затронул по одной строке в каждом из 500 крупных файлов. Без deletion vectors пришлось бы переписать все 500 файлов. С deletion vectors — записать 500 крошечных векторов, файлы данных остаются как есть. Удаление из часов превращается в секунды.
Это идейно то же, что delete files в Iceberg v2 и delta-директории в Hive ACID: общий принцип всех современных форматов — не трогать большие файлы данных, а фиксировать удаления отдельной компактной структурой. Включается свойством таблицы:
-- Включить deletion vectors при создании таблицы
CREATE TABLE delta.ops.accounts (
account_id BIGINT,
status VARCHAR,
balance DECIMAL(12,2)
)
WITH (
deletion_vectors_enabled = true
);
INSERT INTO delta.ops.accounts VALUES
(1, 'active', DECIMAL '1000.00'),
(2, 'closed', DECIMAL '0.00'),
(3, 'active', DECIMAL '750.00');
-- INSERT: 3 rows
-- DELETE пишет deletion vector, не переписывает файл данных
DELETE FROM delta.ops.accounts WHERE status = 'closed';
-- DELETE: 1 row
Change data feed: поток изменений таблицы
Последняя важная возможность Delta — change data feed (CDF), поток изменений. Обычный SELECT отдаёт текущее состояние таблицы. CDF отвечает на другой вопрос: что именно изменилось между двумя версиями таблицы — какие строки вставлены, какие удалены, какие обновлены.
Зачем это нужно. Представьте downstream-витрину, которая агрегирует данные исходной таблицы. После изменения исходной таблицы пересчитывать витрину целиком дорого. Если знать точную дельту изменений, можно обновить витрину инкрементально — применить только изменения. CDF и даёт эту дельту.
Когда у таблицы включён change data feed, Delta при каждой записи дополнительно фиксирует change-события: для каждой затронутой строки — что с ней произошло (insert, delete, update_preimage — было, update_postimage — стало). Эти события можно прочитать за диапазон версий.
-- Включить change data feed на таблице
ALTER TABLE delta.ops.accounts
SET PROPERTIES change_data_feed_enabled = true;
CDF — основа инкрементальных пайплайнов: вместо «прочитать всю таблицу заново» — «прочитать, что изменилось с прошлого запуска». Это перекликается с table_changes() и инкрементальным REFRESH материализованных представлений из модуля по Iceberg: разные форматы, одна инженерная идея — построчное отслеживание изменений ради инкрементальности.
Delta и Iceberg: сравнение
| Аспект | Delta Lake | Iceberg |
|---|---|---|
| Модель метаданных | Transaction log — журнал действий, состояние выводится свёрткой | Дерево снапшотов — каждый снапшот хранит полный список файлов |
| Ускорение чтения метаданных | Checkpoint (свёрнутое состояние в Parquet) | Дерево само избирательно (manifest list -> манифесты) |
| Атомарность коммита | Гонка за следующее имя файла в _delta_log/ | Compare-and-swap указателя в каталоге |
| Удаление без переписывания | Deletion vectors | Delete files (v2), deletion vectors (v3) |
| Time travel | FOR VERSION AS OF, FOR TIMESTAMP AS OF | FOR VERSION AS OF, FOR TIMESTAMP AS OF |
| Поток изменений | Change data feed | table_changes() |
| Роль metastore | Только указатель на директорию таблицы | Один из вариантов каталога |
Delta и Iceberg — конкурирующие форматы одного класса. Оба дают атомарность, time travel, row-level DML, метаданные уровня файла. Trino полноценно поддерживает оба, и выбор между ними чаще диктуется экосистемой вокруг (Delta тесно связан с Databricks/Spark, Iceberg — более вендоронейтрален), чем техническим превосходством. Главное для дата-инженера — понимать, что transaction log и дерево снапшотов решают одну задачу двумя разными способами.
Попробуй сам
В песочнице настройте каталог delta (коннектор delta_lake, metastore, MinIO). Упражнение первое: создайте Delta-таблицу accounts, сделайте несколько INSERT, затем посмотрите в MinIO содержимое поддиректории _delta_log/ — найдите пронумерованные JSON-файлы и откройте один, разберите, какие действия (add, remove) в нём записаны. Упражнение второе на deletion vectors: создайте таблицу с deletion_vectors_enabled = true, загрузите данные, выполните DELETE нескольких строк и проверьте через SELECT, что они исчезли из результата; объясните, почему файл данных при этом не переписался. Упражнение третье на time travel: выполните серию изменений, затем SELECT ... FOR VERSION AS OF <n> для разных версий и проследите, как менялось содержимое. Письменно сравните: чем transaction log Delta отличается от дерева снапшотов Iceberg и почему обе модели дают одинаковые гарантии — атомарность и time travel.