Всем привет! В этой статье хочу рассказать про то, как iceberg работает под капотом, и про то, как он эффективно может взаимодействовать с данными через свою metadata.
Iceberg — табличный формат для больших аналитических наборов данных.
По сути, iceberg — это прослойка между Data Lake и движками запросов, которая с помощью metadata позволяет движкам делать эффективные запросы.
философия iceberg
разделение data и metadata
ACID
Iceberg + sparkSQL
schema evolution и partition evolution
time travel и branch
философия iceberg
metadata iceberg
Metadata хранится в отдельной структуре, оптимизированной для чтения, что позволяет ускорять работу с данными без их полного переиндексирования.
Она состоит из набора таких элементов, как:
metadata.json
snapshot
manifest list
manifest file
data files и служит для оптимизации запросов к Iceberg.
1. metadata.json
metadata.json хранит в себе версию таблицы и ссылки на другие элементы метаданных: схемы, список snapshot и список manifest-файлов.
По сути, является «таблицей версий» и consistency-файлом для всех читающих и пишущих процессов.
2. snapshot
snapshot — зафиксированное состояние таблицы в конкретный момент времени, которое определяет, какие файлы сейчас составляют таблицу.
В snapshot указана ссылка на manifest list.
3. manifest list
Перечисление всех manifest-файлов, которые относятся к конкретному snapshot.
Он нужен для облегчения чтения: вместо прохода по всем файлам Iceberg читает только те, которые реально относятся к запросу.
4. manifest file
Содержит ссылки на данные в рамках конкретного снимка:
путь к файлу
формат
размер
количество строк и сведения о разделах
-
статистики по столбцам
min
max
null-count
и прочие метрики Благодаря manifest-метаданным можно проводить раннюю фильтрацию на уровне файлов, что снижает стоимость выполнения фильтров в движке запросов.
Схема чтения metadata
В момент исполнения запроса движок сначала читает текущий metadata.json -> выбирает нужный snapshot -> получает относящийся к выбранному snapshot manifest list -> подгружает manifest-файлы.

Благодаря структуре чтения metadata, Iceberg позволяет исключить наборы данных, не удовлетворяющие запросу.
1. ACID
Это ключевые гарантии, которые должна обеспечивать БД:
Atomicity
Consistency
Isolation
Durability
Atomicity
Изменения проходят полностью либо вообще не проходят. Рассматривая Atomicity в OLTP-БД, мы видим, что БОЛЬШОЕ количество транзакций изменяет НЕБОЛЬШОЕ количество записей. Это связано с тем, что единицей транзакции является запись. В нашем случае Iceberg — это OLAP-нагрузка, и единицей транзакции является таблица.

Consistency
Все записи и изменения схем приводят к созданию нового snapshot и, следовательно, нового manifest list.
Тем самым Iceberg переводит базу данных из одного корректного состояния в другое.
Isolation
Читатели и писатели не мешают друг другу.
И каждый query видит консистентное состояние таблицы. Iceberg реализует это через snapshot isolation.
Проблема обычного Data Lake
Writer переписывает partition
dt=2026-04-01/-
Reader в этот момент делает SELECT Он может увидеть:
часть старых файлов
часть новых
missing files Inconsistent table state.
Reader и Writer
создаёт новые data files
создаёт новый snapshot
атомарно переключает metadata pointer
Каждый query читает один конкретный snapshot. пример был snapshot 100 files: A, B reader начал запрос. Writer создаёт snapshot 101 files: A, B, C Что увидит reader? Reader продолжит читать snapshot 100: A, B

Writer и Writer
Writer A создаёт snapshot 101 Writer B тоже начал от snapshot 100 Перед commit iceberg проверяет: изменилась ли таблица с начала моего query? Если изменилась, то commit B fail/retry.

Durability
Iceberg не отвечает за надёжное хранение данных.
За durability отвечает само файловое хранилище (HDFS, S3 и т. п.).
Iceberg + sparkSQL
Для начала создадим таблицу где будем собирать данные с датчиков промышленных установок, с колонками:
factories_id: id заводаindustrial_installation_id: id промышленной установкиsensor_id: id датчикаevent_time: время фиксации датчика нового значения
И сделаем партиции по месяцу.
-- создание БД create database factories; -- создани е таблицы с партиционированием create table demo.factories.sensor ( factories_id BIGINT NOT NULL, industrial_installation_id BIGINT NOT NULL, sensor_id BIGINT NOT NULL, value BIGINT NOT NULL, event_time timestamp NOT NULL ) using iceberg PARTITIONED BY ( months(event_time) ); describe table demo.factories.sensor;
Поддержка schema evolution и partition evolution
Schema evolution и partition evolution в Iceberg работают без переписывания всей таблицы благодаря metadata. Она хранит не одну структуру таблицы, а историю версий схем и спецификаций партиций.
schema evolution
add column
Если мы хотим добавить колонки, например:
name_sensor: название датчикаquality_values: признак качества значения
То наша новая схема будет выглядеть так: Пример:
-- добавление колонки quality_values ALTER table demo.factories.sensor ADD columns ( name_sensor string comment 'название датчика', quality_values float comment 'качество значение' ); describe table demo.factories.sensor;
ДО
{ "type": "struct", "schema-id": 0, "fields": [ { "id": 1, "name": "factories_id", "required": true, "type": "long" }, { "id": 2, "name": "industrial_installation_id", "required": true, "type": "long" }, { "id": 3, "name": "sensor_id", "required": true, "type": "long" }, { "id": 4, "name": "value", "required": true, "type": "long" }, { "id": 5, "name": "event_time", "required": true, "type": "timestamptz" } ] }
ПОСЛЕ:
{ "type": "struct", "schema-id": 1, "fields": [ { "id": 1, "name": "factories_id", "required": true, "type": "long" }, { "id": 2, "name": "industrial_installation_id", "required": true, "type": "long" }, { "id": 3, "name": "sensor_id", "required": true, "type": "long" }, { "id": 4, "name": "value", "required": true, "type": "long" }, { "id": 5, "name": "event_time", "required": true, "type": "timestamptz" }, //=========new column========== { "id": 6, "name": "name_sensor", "required": false, "type": "float", "doc": "название датчика" }, { "id": 7, "name": "quality_values", "required": false, "type": "float", "doc": "качество значение" } //============================= ] }
И теперь при чтении старых файлов iceberg понимает, что для старых файлов quality_values и name_sensor = NULL . Надо понимать что при добавлении новых колонок вы не сможете указать NOT NULL, поскольку iceberg поддерживает в схеме консистентность и в старых данных просто нет колонки quality_values.
rename column
Также в Iceberg можно просто переименовывать колонки, ведь мы просто меняем в metadata значение имени колонки. Пример: Мы решили, что неправильно назвали колонку и хотим изменить её имя с event_time: время события, на time_fixation: временная фиксация.
-- изменение имени колонки ALTER table demo.factories.sensor RENAME column event_time to time_fixation; describe table demo.factories.sensor;
ДО:
{ "type": "struct", "schema-id": 1, "fields": [ { "id": 1, "name": "factories_id", "required": true, "type": "long" }, { "id": 2, "name": "industrial_installation_id", "required": true, "type": "long" }, { "id": 3, "name": "sensor_id", "required": true, "type": "long" }, { "id": 4, "name": "value", "required": true, "type": "long" }, { "id": 5, "name": "event_time", "required": true, "type": "timestamptz" }, { "id": 6, "name": "name_sensor", "required": false, "type": "float", "doc": "название датчика" }, { "id": 7, "name": "quality_values", "required": false, "type": "float", "doc": "качество значение" } ] }
ПОСЛЕ:
{ "type": "struct", "schema-id": 2, "fields": [ { "id": 1, "name": "factories_id", "required": true, "type": "long" }, { "id": 2, "name": "industrial_installation_id", "required": true, "type": "long" }, { "id": 3, "name": "sensor_id", "required": true, "type": "long" }, { "id": 4, "name": "value", "required": true, "type": "long" }, //=========rename column========== { "id": 5, "name": "time_fixation", "required": true, "type": "timestamptz" }, //================================ { "id": 6, "name": "name_sensor", "required": false, "type": "float", "doc": "название датчика" }, { "id": 7, "name": "quality_values", "required": false, "type": "float", "doc": "качество значение" } ] }
delete column
При удалении колонки Iceberg просто перестаёт читать данные, относящиеся к ней.
-- удаление колонки ALTER table demo.factories.sensor drop column name_sensor; describe table demo.factories.sensor;
ДО:
{ "type": "struct", "schema-id": 2, "fields": [ { "id": 1, "name": "factories_id", "required": true, "type": "long" }, { "id": 2, "name": "industrial_installation_id", "required": true, "type": "long" }, { "id": 3, "name": "sensor_id", "required": true, "type": "long" }, { "id": 4, "name": "value", "required": true, "type": "long" }, { "id": 5, "name": "time_fixation", "required": true, "type": "timestamptz" }, { "id": 6, "name": "name_sensor", "required": false, "type": "float", "doc": "название датчика" }, { "id": 7, "name": "quality_values", "required": false, "type": "float", "doc": "качество значение" } ] }
ПОСЛЕ:
{ "type": "struct", "schema-id": 3, "fields": [ { "id": 1, "name": "factories_id", "required": true, "type": "long" }, { "id": 2, "name": "industrial_installation_id", "required": true, "type": "long" }, { "id": 3, "name": "sensor_id", "required": true, "type": "long" }, { "id": 4, "name": "value", "required": true, "type": "long" }, { "id": 5, "name": "time_fixation", "required": true, "type": "timestamptz" }, //=========drop column========== //============================== { "id": 7, "name": "quality_values", "required": false, "type": "float", "doc": "качество значение" } ] }
reorder columns
просто меняется позиция в списке Пример: Физически parquet может хранить: `[id][name][age]
ALTER TABLE demo.factories.sensor ALTER COLUMN time_fixation AFTER quality_values;
ДО:
{ "type": "struct", "schema-id": 3, "fields": [ { "id": 1, "name": "factories_id", "required": true, "type": "long" }, { "id": 2, "name": "industrial_installation_id", "required": true, "type": "long" }, { "id": 3, "name": "sensor_id", "required": true, "type": "long" }, { "id": 4, "name": "value", "required": true, "type": "long" }, { "id": 5, "name": "time_fixation", "required": true, "type": "timestamptz" }, { "id": 7, "name": "quality_values", "required": false, "type": "float", "doc": "качество значение" } ] }
ПОСЛЕ:
{ "type": "struct", "schema-id": 4, "fields": [ { "id": 1, "name": "factories_id", "required": true, "type": "long" }, { "id": 2, "name": "industrial_installation_id", "required": true, "type": "long" }, { "id": 3, "name": "sensor_id", "required": true, "type": "long" }, { "id": 4, "name": "value", "required": true, "type": "long" }, { "id": 7, "name": "quality_values", "required": false, "type": "float", "doc": "качество значение" }, { "id": 5, "name": "time_fixation", "required": true, "type": "timestamptz" } ] }
Обратите внимание, что в схемах iceberg у каждой колонки есть immutable column ID, то есть даже при добавлении, изменении порядка или удаления колонки id всегда будет один и тот же. Благодаря этому id, iceberg понимает какую колонку надо вытащить из колонко-ориентированных файлов а не меняет структуры их самих. id схемы = номеру колонки в файле
partition evolution
Так же, как и со схемами, всё завязано не на переписывании самих данных, а на изменении metadata.
add partition
Пример: Мы хотим сделать не только партиционирование по месяцу, но и например по id предприятия.
-- добавление партиций ALTER TABLE demo.factories.sensor ADD PARTITION FIELD factories_id; describe table demo.factories.sensor;
ДО:
{ "spec-id": 0, "fields": [ { "name": "event_time_month", "transform": "month", "source-id": 5, "field-id": 1000 } ] }
ПОСЛЕ:
{ "spec-id": 1, "fields": [ { "name": "event_time_month", "transform": "month", "source-id": 5, "field-id": 1000 }, { "name": "factories_id", "transform": "identity", "source-id": 1, "field-id": 1001 } ] }
replace partition
Пример:
у нас было партиционирование по месяцу months(event_time), а мы хотим делать партиции по дням days(event_time).
-- изменение существующей партиции ALTER TABLE demo.factories.sensor REPLACE PARTITION FIELD months(time_fixation) WITH days(time_fixation); describe table demo.factories.sensor;
ДО:
{ "spec-id": 1, "fields": [ { "name": "event_time_month", "transform": "month", "source-id": 5, "field-id": 1000 }, { "name": "factories_id", "transform": "identity", "source-id": 1, "field-id": 1001 } ] }
ПОСЛЕ:
{ "spec-id": 2, "fields": [ { "name": "factories_id", "transform": "identity", "source-id": 1, "field-id": 1001 }, { "name": "time_fixation_day", "transform": "day", "source-id": 5, "field-id": 1002 } ] }
И получается, что старые и новые данные физически разбиты по-разному, но для пользователя это остаётся одной таблицей.
drop partition
--удаление партиции ALTER TABLE demo.factories.sensor DROP PARTITION FIELD factories_id describe table demo.factories.sensor;
ДО:
{ "spec-id": 2, "fields": [ { "name": "factories_id", "transform": "identity", "source-id": 1, "field-id": 1001 }, { "name": "time_fixation_day", "transform": "day", "source-id": 5, "field-id": 1002 } ] }
ПОСЛЕ:
{ "spec-id": 3, "fields": [ { "name": "time_fixation_day", "transform": "day", "source-id": 5, "field-id": 1002 } ] }
Time travel и branch
Функция time travel позволяет получить данные в том виде, в котором они были в конкретный момент времени, благодаря snapshot. Каждый snapshot представляет собой полную и согласованную версию таблицы на определённый момент времени.
Branch — это развитие идеи time travel, очень похожее на Git.
Обычный Time travel

В iceberg с каждой операцией на изменение таблицы создается новый snapshot. Если их не чистить metadata растет, planning замедляется, а storage раздувается.
Для решения этой проблемы можно указать время жизни и минимальное количество snapshot:
ALTER TABLE demo.factories.sensor_pyrolysis SET TBLPROPERTIES ( 'history.expire.max-snapshot-age-ms'='604800000', 'history.expire.min-snapshots-to-keep'='10' );
а сама очистка происходит с помощью команды:
CALL demo.system.expire_snapshots( table => 'demo.factories.sensor' )
Важный момент, iceberg является табличным форматом, а не полноценной БД, поэтому он сам автоматически не будет удалять старые snapshot.
Time travel с branch

Благодаря branch можно не только путешествовать в конкретное время, но и:
тестировать новые варианты хранения и работы с данными без влияния на производственные данные;
строить аналитику и отчётность с разными аналитическими моделями.
Заключение
Вся философия и весь принцип работы Iceberg заключается в metadata.
Благодаря ей мы можем эффективно выполнять запросы, получать статистику по колонкам, изменять схемы, не тратя огромные ресурсы каждый раз, использовать time travel и branch для тестирования фичей и командной работы, изменять схемы и партиции, не переписывая сами файлы с данными.
Strike14
Доступное объяснение как iceberg работает с метадатой