Зачем вообще нужен table format
Object storage вроде S3, GCS или MinIO — это не файловая система. Это key-value хранилище, где ключ — длинная строка, а значение — байты объекта. Никаких директорий там физически нет: «папка» — это иллюзия, которую даёт префикс ключа и операция листинга по этому префиксу. Атомарного «переименуй директорию» там тоже нет. Эти два факта — отсутствие настоящих директорий и отсутствие атомарного rename — ломают всё, на чём годами держался data lake поверх Hive.
Apache Iceberg — это table format: спецификация того, как разложить набор файлов данных и метаданных так, чтобы поверх «глупого» хранилища объектов появились настоящая таблица, транзакции и история. Не движок запросов и не формат файла. Данные по-прежнему лежат в
Шаг 1: чем плох Hive table format на object storage
Чтобы оценить Iceberg, надо понять, что он чинит. В Hive table format таблица — это директория, а партиция — поддиректория вида country=RU/dt=2026-06-01/. Где лежат данные таблицы, нигде явно не записано: чтобы это узнать, движок листит директории. Отсюда три фундаментальные боли на S3.
Листинг дорогой и неконсистентный. Чтобы прочитать таблицу, надо рекурсивно перечислить все объекты под префиксом. На таблице с десятками тысяч партиций это тысячи LIST-запросов к S3, каждый с задержкой в десятки миллисекунд. Планирование запроса само по себе занимает минуты — ещё до чтения первого байта данных. Хуже того: листинг возвращает ровно то, что физически лежит под префиксом прямо сейчас, поэтому состояние таблицы определяется содержимым бакета, а не явным манифестом.
Нет атомарности на уровне таблицы. Запись новой партиции — это серия PUT отдельных файлов. Читатель, листящий директорию в середине записи, увидит половину файлов: грязное чтение в чистом виде. Никакого commit-барьера, который бы делал набор файлов видимым «всё-или-ничего», нет.
Rename не атомарен и не бесплатен. Классический трюк Hive — писать во временную директорию, а в конце атомарно её переименовать. На HDFS rename — это правка указателя в namenode, мгновенная и атомарная. На S3 rename не существует как операция: это COPY каждого объекта плюс DELETE оригинала. Для тысяч файлов это дорого, медленно и в середине процесса оставляет таблицу в полусобранном состоянии. Именно на этом «коммите через rename» молча рассыпается надёжность Hive-таблиц на облачном хранилище.
Корень всех трёх проблем один: в Hive состояние таблицы выводится из листинга директорий, а не задаётся явно. Iceberg переворачивает это.
Шаг 2: дерево метаданных — состояние задаётся явно
Iceberg никогда не листит директории, чтобы понять, что лежит в таблице. Вместо этого он держит явное, иммутабельное дерево метаданных. Сверху вниз цепочка такая:
catalog (указатель на текущий metadata.json)
|
v
metadata.json ── схемы, partition specs, список snapshots, current-snapshot-id
|
v
manifest list (Avro) ── один файл на snapshot; список manifest'ов + их partition-границы
|
v
manifest files (Avro) ── списки data-файлов + статистика (min/max, null-counts, row-count)
|
v
data files (Parquet/ORC/Avro) ── собственно строки
Разберём уровни, потому что вся механика ACID и планирования живёт именно здесь.
Catalog хранит ровно одну вещь на таблицу: указатель на текущий файл metadata.json. Это может быть строка в Postgres (JDBC-каталог), запись в Hive Metastore, объект в REST-каталоге или AWS Glue. Важна не реализация, а то, что у каталога есть одна обновляемая ячейка — «вот актуальная версия таблицы».
metadata.json — корень дерева на конкретный момент. Это иммутабельный JSON-файл, описывающий всю таблицу: историю partition spec, массив всех snapshots и поле current-snapshot-id. Каждый коммит порождает новый файл metadata.json — старый не трогается.
Manifest list — один Avro-файл на snapshot. Это «оглавление» снимка: список manifest-файлов, входящих в него, и для каждого — агрегированные границы значений партиционирующих колонок (partition field summaries). По этим границам читатель может выкинуть целый manifest, даже не открыв его.
Manifest file — Avro-файл со списком конкретных data-файлов. Для каждого файла хранятся его путь, формат, к какой партиции относится и — ключевое — column-level статистика: row_count, нижние и верхние границы (lower_bounds/upper_bounds) по колонкам, счётчики null. Именно эта статистика позволяет планировать запрос без чтения данных.
Data files — обычные Parquet-файлы. Иммутабельны: Iceberg никогда не дописывает в существующий файл и не правит его in-place.
Ключевое свойство всего дерева: оно построено только из иммутабельных файлов плюс один обновляемый указатель в каталоге. Запомните это — на нём держится весь следующий шаг.
Шаг 3: snapshot и атомарный commit — откуда берётся ACID и time travel
- Пишутся новые data-файлы (новые объекты в S3).
- Пишется новый manifest, ссылающийся на актуальный набор data-файлов.
- Пишется новый manifest list для нового snapshot.
- Пишется новый
metadata.json, где этот snapshot становитсяcurrent-snapshot-id, а предыдущий остаётся в истории. - Каталог атомарно меняет указатель со старого
metadata.jsonна новый.
Вся транзакция сводится к одному действию: атомарная замена одного указателя в каталоге. Это реализуется через UPDATE ... WHERE metadata_location = :ожидаемый. Это
Отсюда напрямую следуют гарантии:
- Атомарность. Пока указатель не переключён, новые файлы для читателей не существуют. Переключение указателя одной операцией делает весь набор изменений видимым разом. Нет состояния «половина записана».
- Изоляция (snapshot isolation). Читатель резолвит таблицу через текущий
metadata.jsonв момент старта запроса и работает с этим snapshot до конца. Параллельная запись создаёт новый snapshot, но читатель его не видит — он держит консистентный снимок. - Долговечность. Каждый snapshot — это иммутабельные файлы. Откатить ничего не нужно: при сбое до CAS просто остаются «осиротевшие» файлы, которые подберёт уборка.
Time travel же оказывается бесплатным побочным эффектом. Раз каждый snapshot иммутабелен и остаётся в истории metadata.json, пока на него ссылаются, можно прочитать таблицу как она выглядела на любой исторический snapshot — по его id или по timestamp:
-- состояние на конкретный snapshot
SELECT * FROM db.orders VERSION AS OF 3821550127947089009;
-- состояние на момент времени
SELECT * FROM db.orders FOR TIMESTAMP AS OF '2026-05-28 09:00:00';
Никакой особой машинерии для time travel нет: вы просто говорите движку резолвить дерево не от current-snapshot-id, а от исторического. Та же иммутабельность даёт мгновенный откат (rollback переключает указатель на прошлый snapshot) и аудит — кто и что записал в каждый коммит. На уровне V2-спеки snapshots ещё нумеруются монотонными sequence numbers, что задаёт порядок и нужно для row-level операций ниже.
Шаг 4: hidden partitioning и эволюция партиций
В Hive, чтобы партиционирование сработало, инженер должен материализовать производный столбец и сам помнить о нём в каждом запросе. Партиционировали по event_day, выведенному из event_ts? Тогда WHERE event_ts >= '2026-06-01' не отсечёт ни одной партиции — фильтр стоит по event_ts, а партиции нарезаны по event_day. Чтобы pruning сработал, придётся писать WHERE event_day = '2026-06-01'. Забыл — получил полное сканирование. Это вечный источник тихих просадок производительности.
Iceberg вводит hidden partitioning. В partition spec вы объявляете не отдельный столбец, а transform над реальным столбцом: day(event_ts), bucket(16, user_id), truncate(10, name). Само значение партиции вычисляется и хранится в метаданных; отдельной колонки в данных нет. Когда вы пишете WHERE event_ts >= '2026-06-01', Iceberg сам применяет тот же transform к границам предиката и отсекает партиции. Запрос остаётся на естественном столбце — таблица сама знает, как из него получить партицию.
Второе следствие иммутабельных метаданных — partition evolution. Раз каждый snapshot ссылается на конкретный partition spec, а metadata хранит все исторические spec, схему партиционирования можно сменить без переписывания старых данных. Старые файлы остаются под старым spec, новые пишутся под новый; manifest для каждого файла помнит, какому spec он принадлежит. На чтении движок планирует мульти-spec: к старым файлам применяет старый transform, к новым — новый. Перешли с month(ts) на day(ts), потому что данных стало больше? Никакого простоя и переписывания петабайтов — только новая запись в metadata.
Шаг 5: reader — планирование по статистике вместо листинга
Теперь становится понятно, почему планирование в Iceberg на порядки дешевле Hive. Читатель никогда не листит директории. Он спускается по дереву, на каждом уровне отсекая лишнее по статистике:
- Каталог отдаёт указатель на
metadata.json→ читатель резолвитcurrent-snapshot-idи его manifest list. - По partition-границам в manifest list читатель выбрасывает целые manifest-файлы, чьи диапазоны не пересекаются с предикатом запроса (manifest pruning).
- В оставшихся manifest читатель смотрит per-file
lower_bounds/upper_boundsи выбрасывает data-файлы, чьи min/max не пересекаются с фильтром (data skipping). ЗапросWHERE amount > 1000пропустит файл, у которогоupper_bound(amount) = 980, даже не открыв его. - Остаётся точный список data-файлов на чтение. Только тут идёт обращение к S3 за самими данными.
Планирование — это чтение нескольких Avro-файлов метаданных и арифметика над границами, а не тысячи LIST-запросов. На больших таблицах это разница между минутами и десятками миллисекунд.
Последний внутренний выбор — как обновлять и удалять строки, раз data-файлы иммутабельны. Здесь две стратегии.
- Copy-on-write (CoW). При
UPDATE/DELETEIceberg переписывает целиком каждый затронутый data-файл с применёнными изменениями и фиксирует новый snapshot. Запись дороже (переписывается весь файл ради нескольких строк), зато чтение максимально быстрое — лишней работы на reader нет. - Merge-on-read (MoR). Вместо переписывания пишется компактный delete-файл (позиционные или equality deletes), помечающий удалённые/изменённые строки. Запись дешёвая и быстрая, но reader на лету мерджит data-файлы с delete-файлами, и чтение дороже, пока фоновая компакция их не свернёт.
CoW оптимизирует под чтение, MoR — под запись. Это прямая ручка под нагрузку: редкие крупные батчи против частых мелких upsert’ов из стрима.
Hive vs Iceberg: сводка
| Свойство | Hive table format | Apache Iceberg |
|---|---|---|
| Состояние таблицы | выводится из листинга директорий | задано явно в дереве метаданных |
| Планирование запроса | тысячи LIST по S3 | чтение manifest + отсев по min/max |
| Атомарность коммита | нет (серия PUT, rename) | атомарный CAS указателя в каталоге |
| Коммит на S3 | COPY + DELETE каждого файла | замена одного указателя |
| Изоляция чтения | грязные чтения при записи | snapshot isolation |
| Time travel | нет | по snapshot id или timestamp |
| Partition pruning | нужен фильтр по производному столбцу | hidden partitioning, фильтр по исходному |
| Смена партиционирования | переписать всю таблицу | partition evolution без переписи |
| Удаление/обновление строк | переписать партицию | CoW или MoR (delete-файлы) |
Соберём ментальную модель
Один тезис держит всё вместе: Iceberg заменяет «состояние таблицы = что лежит в бакете» на «состояние таблицы = на что указывает каталог». Из этой замены механически вытекает всё остальное. Иммутабельное дерево метаданных плюс один атомарно переключаемый указатель дают атомарность и изоляцию. Сохранение старых указателей даёт time travel и откат. Хранение transform’ов и всех partition spec в метаданных даёт hidden partitioning и эволюцию партиций. А per-file статистика в манифестах заменяет дорогой листинг на дешёвое планирование. Не пять отдельных фич — одно архитектурное решение, повёрнутое разными гранями.
Дальше начинается инженерия эксплуатации: компакция мелких файлов, expire snapshots (иначе история и осиротевшие файлы растут бесконечно), REST-каталоги, выбор CoW против MoR под конкретный паттерн нагрузки, сравнение с Delta Lake и Hudi. Но без модели «каталог → metadata → manifest list → manifest → data + один атомарный CAS» всё это — набор команд без понимания, почему они работают.
Если хочется системно и до самого железа — посмотрите направление Data Engineering: всё внутри-к-железу, с интерактивными песочницами и нарисованными от руки диаграммами.
Эта статья — бесплатный standalone-разбор. Если же хочется разобрать Iceberg побайтово — реальные Avro-манифесты, протокол коммита, CoW против MoR на живых данных — это часть курса Modern storage formats, где Iceberg идёт после Parquet, ORC и Delta Lake. Курс платный, но вводные уроки открыты бесплатно — начните с них.