Зачем вообще нужен table format
Начнём с ментальной модели, потому что без неё спор Iceberg vs Delta vs Hudi превращается в перечисление фич. Object storage (S3, GCS, ADLS) умеет ровно одно: положить объект по ключу и отдать его по ключу. Никаких транзакций, никакого RENAME как атомарной операции, никакого UPDATE WHERE. У вас просто свалка из миллионов файлов parquet.
Запрос SELECT поверх такой свалки решается двумя вопросами: какие файлы относятся к таблице прямо сейчас и какие из них можно пропустить, не читая. Если на эти вопросы отвечает листинг директории (s3 ls) — у вас Hive-таблица со всеми её болячками: листинг префикса в S3 это тысячи API-вызовов, нет атомарности при дозаписи, читатель может увидеть половину коммита, а schema evolution ломает старые файлы.
Open table format — это слой метаданных поверх файлов данных, который превращает свалку в таблицу с
Из этой одной идеи вырастает всё остальное:
- Time travel — старые манифесты не удаляются, можно прочитать таблицу «как на вчера».
- Schema evolution — схема хранится в метаданных, а не выводится из файлов; переименование колонки это правка метаданных, а не переписывание данных.
- Data skipping — по min/max статистике движок отбрасывает целые файлы, не открывая их.
- ACID-коммит — атомарная смена «указателя» на актуальный набор манифестов.
Различия между Iceberg, Delta и Hudi — это различия в том, как именно устроен этот слой метаданных и как происходит атомарный коммит. Всё остальное (движки, lock-in, производительность upsert) — следствия этих архитектурных решений.
Как устроен атомарный коммит
Самый честный способ понять формат — посмотреть, где живёт «истина» о том, что является текущей версией таблицы. Атомарность коммита держится на одной маленькой атомарной операции, и у каждого формата она своя.
Apache Iceberg. Дерево метаданных: корневой файл metadata.json ссылается на manifest list, тот — на manifest files, те — на data files. Новый коммит создаёт новую цепочку и новый metadata.json. Атомарность достигается через catalog — внешний компонент (Hive Metastore, AWS Glue, REST-каталог, Nessie), который атомарно меняет указатель «текущий metadata.json для этой таблицы» через compare-and-swap. Каталог — обязательная часть Iceberg; именно он, а не файловая система, решает, кто выиграл гонку при конкурентной записи.
Delta Lake. Сердце — директория _delta_log/ рядом с данными. Это упорядоченный лог транзакций: файлы 00000000000000000000.json, ...001.json и так далее, плюс периодические checkpoint в parquet. Каждый коммит — новый пронумерованный JSON-файл с действиями (add file, remove file, metaData). Атомарность держится на том, что создание файла N.json должно быть атомарным и эксклюзивным: побеждает тот, кто первым создал файл с этим номером. На HDFS это работает из коробки, а на S3 (где нет атомарного put-if-absent исторически) Delta полагается на внешнюю координацию.
Apache Hudi. Своя .hoodie/: последовательность инстантов (commit, deltacommit, compaction, clean), каждый в трёх состояниях — requested, inflight, completed. Hudi изначально проектировался под record key с файлом, где запись уже лежит. Это принципиальное отличие: Hudi знает, где находится конкретная запись по ключу, тогда как Iceberg и Delta оперируют на уровне файлов целиком.
Запомните этот водораздел: Iceberg делегирует атомарность каталогу, Delta — атомарному созданию файла лога, Hudi строит вокруг record-level индекса. Дальше всё выводится из этого.
Конкурентная запись хорошо иллюстрирует разницу. Все три формата используют compare-and-swap одной строки в Glue или REST-каталоге. У Delta — за создание файла N.json; проигравший видит, что N.json уже существует, читает его и ретраит на N+1. У Hudi конкуренцию по записи в один файл-группу разруливает lock provider (ZooKeeper, DynamoDB, HMS) — без него многопоточная запись в одни и те же партиции небезопасна. Практический вывод: на S3 ни один формат не «бессерверный» в чистом виде — где-то рядом обязательно живёт компонент, обеспечивающий атомарность.
Чем data skipping отличается между форматами
Манифесты у всех хранят min/max-статистику, но способ её применения различается. Iceberg кладёт статистику в manifest files и при планировании запроса фильтрует файлы ещё до их открытия — это даёт planning без листинга S3 и работает тем лучше, чем аккуратнее данные отсортированы по фильтруемым колонкам. Delta хранит статистику в JSON-действиях add внутри лога и сворачивает её в checkpoint; при больших таблицах именно checkpoint спасает от чтения тысяч мелких JSON. Hudi полагается на индекс по record key плюс column stats; для точечных upsert-ов это решает, но для аналитических range-сканов Hudi исторически уступает Iceberg по чистоте data skipping.
Copy-on-write vs merge-on-read
Главная ось, по которой расходятся форматы на практике — что происходит при UPDATE/DELETE/MERGE одной строки в большом файле. Object storage immutable: переписать байты внутри объекта нельзя, можно только перезаписать объект целиком.
Copy-on-write (COW). Меняете одну строку — формат читает весь файл данных, применяет изменение и пишет новый файл целиком; старый помечается удалённым в метаданных. Чтение остаётся максимально быстрым (читаем готовые файлы, никакого слияния), но запись дорогая:
Merge-on-read (MOR). Изменение пишется в маленький дельта-файл (новые/изменённые записи или delete-векторы), а актуальное состояние строки вычисляется на чтении, сливая базовый файл с дельтами. Запись дешёвая и быстрая, но чтение медленнее (нужно мерджить) — пока фоновый
Как это поддержано в каждом формате:
- Delta — исторически чистый COW. Начиная с deletion vectors
DELETE/UPDATEстали MOR-подобными: строки не переписываются, а помечаются в bitmap deletion vector, который читатель применяет на лету. - Iceberg — поддерживает оба режима через
write.delete.mode/write.update.mode(copy-on-writeилиmerge-on-read). MOR в v2 реализован через position deletes и equality deletes; в v3 появились deletion vectors, унифицированные с Delta-подходом. - Hudi — два типа таблицы выбираются явно при создании:
Copy On WriteиMerge On Read. MOR-таблица пишет в row-based лог-файлы (avro-подобные log blocks), которые компакшен периодически сворачивает в columnar базовые файлы.
-- Iceberg: переключение update на merge-on-read для upsert-heavy таблицы
ALTER TABLE events SET TBLPROPERTIES (
'write.update.mode' = 'merge-on-read',
'write.delete.mode' = 'merge-on-read',
'write.merge.mode' = 'merge-on-read'
);
-- Дальше обычный MERGE: дельты пишутся быстро, чтение мерджит на лету
MERGE INTO events t USING updates s ON t.id = s.id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;
Правило выбора режима простое: много мелких частых изменений (CDC, streaming upsert) → MOR; редкие батчевые перезаписи и приоритет скорости чтения → COW. Не забывайте, что MOR без агрессивного compaction деградирует: число дельт растёт, чтение замедляется, мелкие файлы плодятся.
Поддержка движков и engine-lock-in
Тут проходит самая важная для бизнеса граница, и её часто недооценивают.
Apache Iceberg изначально проектировался как нейтральный к движку. Spec открытая, реализации читателя/писателя есть в Spark, Trino, Flink, Presto, Dremio, Snowflake, BigQuery, DuckDB. Это де-факто отраслевой стандарт для мульти-движкового лейкхауса: одну таблицу пишет Spark, тот же снапшот читает Trino для ad-hoc и Flink для стриминга — без дублирования данных.
Delta Lake технически open source (формат и Delta Standalone / delta-rs), но реальная гравитация — экосистема Databricks. Полная фича-поверхность (liquid clustering, оптимальный compaction, deletion vectors, наиболее свежие возможности) первоклассно работает именно в Spark на Databricks. Сторонние движки читают Delta, но часто с задержкой по фичам или в read-only режиме. Это и есть мягкий
Apache Hudi глубоко интегрирован со Spark и Flink, особенно для стриминговых пайплайнов (DeltaStreamer / Hudi Streamer). Trino и Presto читают Hudi, но запись и весь богатый upsert-функционал (индексы, инкрементальные запросы) живут прежде всего в Spark/Flink. Hudi — самый «движок-специфичный» в плане записи из троицы.
запись чтение (де-факто)
Iceberg Spark/Flink/Trino Spark/Trino/Flink/Snowflake/BigQuery/DuckDB
Delta Spark (Databricks) Spark + read-only коннекторы; UniForm -> Iceberg
Hudi Spark/Flink Spark/Flink/Trino (read), upsert -> Spark/Flink
Сводная таблица сравнения
| Критерий | Iceberg | Delta Lake | Hudi |
|---|---|---|---|
| Модель метаданных | дерево manifest + catalog | лог _delta_log/ + checkpoints | timeline .hoodie/ + индекс |
| Атомарность коммита | catalog compare-and-swap | атомарное создание N.json | timeline инстанты + lock provider |
| Default режим записи | COW, MOR опционально | COW + deletion vectors | выбор COW / MOR при создании |
| Record-level upsert | по файлам (нет индекса) | по файлам | да, через record key + индекс |
| Time travel | да (snapshot id / timestamp) | да (version / timestamp) | да (instant time) |
| Schema evolution | полная (add/drop/rename/reorder) | полная | полная |
| Hidden partitioning | да (partition transforms) | нет (явные колонки) | нет (явные колонки) |
| Engine-нейтральность | высокая (стандарт) | средняя (тяготеет к Databricks) | средняя (тяготеет к Spark/Flink) |
| Лучший сценарий | мульти-движковый лейкхаус | стек на Databricks/Spark | upsert-heavy CDC, стриминг |
Как выбирать на практике
Берите Iceberg по умолчанию, если вы строите вендор-нейтральный лейкхаус и хотите читать одни и те же данные из Spark, Trino, Flink и облачных warehouse без миграций. Hidden partitioning (партиционирование как функция от колонки, скрытое от запроса) убирает классический Hive-грабли, когда забыли добавить фильтр по партиционной колонке. Это самый безопасный долгосрочный выбор по интероперабельности.
Берите Delta Lake, если ваш стек уже на Databricks или плотно на Spark. Внутри этой экосистемы Delta зрелый, отлаженный и даёт лучший developer experience. Просто заранее честно оцените стоимость выхода: чем больше эксклюзивных фич вы используете, тем сильнее operational lock-in, даже при формально открытом формате.
Берите Hudi, если ваша доминирующая нагрузка — upsert-heavy
Чего не стоит делать: выбирать формат по бенчмаркам из чужого блога. Цифры COW vs MOR полностью зависят от соотношения чтений и записей, размера файлов и того, настроен ли у вас compaction. Сначала определите профиль нагрузки (батч vs стриминг, read-heavy vs upsert-heavy, один движок vs много), и формат выберется почти однозначно.
Отдельно про операционную стоимость, которую новички упускают. Любой из трёх форматов без maintenance деградирует одинаково: накапливаются мелкие файлы, растёт число снапшотов и delete-файлов, метаданные раздуваются, и планирование запроса само по себе начинает тормозить. Поэтому в проде у вас всегда есть фоновые джобы: compaction (свернуть мелкие файлы и дельты в крупные базовые), expire snapshots / vacuum (удалить старые версии и осиротевшие файлы за пределами окна time travel), и rewrite manifests (перебалансировать метаданные). Окно time travel — это прямой trade-off: чем дольше храните историю, тем больше платите за хранение и тем дороже vacuum. Iceberg выносит это в процедуры (rewrite_data_files, expire_snapshots), Delta — в команды OPTIMIZE и VACUUM, Hudi запускает clean и compaction в рамках своей timeline. Выбирая формат, вы выбираете и оператора этих джоб: на Databricks многое автоматизировано, в self-managed Iceberg/Trino вы настраиваете это руками.
И последнее по lock-in: миграция между форматами реальна, но не бесплатна. Конвертеры (delta-rs, Iceberg add_files, UniForm) умеют переиспользовать те же parquet-файлы данных и переписать только метаданный слой — данные не дублируются. Но эксклюзивные фичи (Delta liquid clustering, Hudi record index, Iceberg hidden partitioning) при переезде теряются или требуют ручного маппинга. Поэтому правильнее с самого начала закладывать формат под профиль нагрузки, а не «потом перепишем».
Что дальше
Эта статья — карта местности. Реальное мастерство начинается там, где вы открываете metadata.json, manifest list и _delta_log/ руками: считаете число манифестов, смотрите на накопленные delete-файлы, диагностируете small-files problem, настраиваете compaction и retention для time travel так, чтобы они не съели весь бакет.
Эту внутрянку — байт за байтом, со всеми тремя форматами на одном датасете — мы разбираем в премиум-курсе Modern storage formats. Вводный модуль с фундаментом (Parquet, манифесты, ACID-механика) открыт бесплатно как тизер — этого достаточно, чтобы понять, на каком слое вообще живут ваши таблицы. Если вы только выбираете направление, начните с обзора инженерии данных.
Открыть курс Modern storage formats — вводный модуль бесплатно