Skip to content
Learning Platform

Зачем вообще нужен 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 — это слой метаданных поверх файлов данных, который превращает свалку в таблицу с ACID-гарантиями. Ключевая идея у всех трёх форматов одна: вместо листинга директории читатель открывает манифест — отдельный набор метаданных, который явно перечисляет, какие файлы входят в актуальный snapshot таблицы, и хранит по каждому файлу статистику (min/max значений колонок, число строк, null-каунты).

Из этой одной идеи вырастает всё остальное:

  • 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. Своя timeline в директории .hoodie/: последовательность инстантов (commit, deltacommit, compaction, clean), каждый в трёх состояниях — requested, inflight, completed. Hudi изначально проектировался под upsert-нагрузку, поэтому у него есть индекс, который сопоставляет record key с файлом, где запись уже лежит. Это принципиальное отличие: Hudi знает, где находится конкретная запись по ключу, тогда как Iceberg и Delta оперируют на уровне файлов целиком.

Запомните этот водораздел: Iceberg делегирует атомарность каталогу, Delta — атомарному созданию файла лога, Hudi строит вокруг record-level индекса. Дальше всё выводится из этого.

Конкурентная запись хорошо иллюстрирует разницу. Все три формата используют optimistic concurrency: писатель читает текущую версию, готовит коммит, и в конце пытается атомарно его применить. Если за это время кто-то уже сдвинул указатель, проигравший должен пересчитать коммит относительно новой базы или упасть с конфликтом. У Iceberg арбитром выступает каталог: два писателя соревнуются за 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). Меняете одну строку — формат читает весь файл данных, применяет изменение и пишет новый файл целиком; старый помечается удалённым в метаданных. Чтение остаётся максимально быстрым (читаем готовые файлы, никакого слияния), но запись дорогая: write amplification огромная — изменили 1 строку из миллиона, переписали миллион.

Merge-on-read (MOR). Изменение пишется в маленький дельта-файл (новые/изменённые записи или delete-векторы), а актуальное состояние строки вычисляется на чтении, сливая базовый файл с дельтами. Запись дешёвая и быстрая, но чтение медленнее (нужно мерджить) — пока фоновый compaction не свернёт дельты обратно в базовые файлы.

Как это поддержано в каждом формате:

  • 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 режиме. Это и есть мягкий engine-lock-in: формат открыт, но операционно вы привязаны к одному вендору. UniForm (генерация Iceberg-метаданных поверх Delta-таблиц) — признание самой Databricks, что Iceberg выиграл войну за интероперабельность.

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

Сводная таблица сравнения

КритерийIcebergDelta LakeHudi
Модель метаданныхдерево manifest + catalogлог _delta_log/ + checkpointstimeline .hoodie/ + индекс
Атомарность коммитаcatalog compare-and-swapатомарное создание N.jsontimeline инстанты + 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/Sparkupsert-heavy CDC, стриминг

Как выбирать на практике

Берите Iceberg по умолчанию, если вы строите вендор-нейтральный лейкхаус и хотите читать одни и те же данные из Spark, Trino, Flink и облачных warehouse без миграций. Hidden partitioning (партиционирование как функция от колонки, скрытое от запроса) убирает классический Hive-грабли, когда забыли добавить фильтр по партиционной колонке. Это самый безопасный долгосрочный выбор по интероперабельности.

Берите Delta Lake, если ваш стек уже на Databricks или плотно на Spark. Внутри этой экосистемы Delta зрелый, отлаженный и даёт лучший developer experience. Просто заранее честно оцените стоимость выхода: чем больше эксклюзивных фич вы используете, тем сильнее operational lock-in, даже при формально открытом формате.

Берите Hudi, если ваша доминирующая нагрузка — upsert-heavy CDC: поток изменений из OLTP-базы, где постоянно прилетают update/delete по первичному ключу, и нужны инкрементальные запросы («дай всё, что изменилось с инстанта X»). Record-level индекс и MOR-таблицы — ровно та архитектура, под которую Hudi затачивался с самого начала.

Чего не стоит делать: выбирать формат по бенчмаркам из чужого блога. Цифры 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 — вводный модуль бесплатно

Ещё в направлении · Data Engineering

Все материалы направления →