Всем привет! В этой статье хочу рассказать про то, как iceberg работает под капотом, и про то, как он эффективно может взаимодействовать с данными через свою metadata.

Icebergтабличный формат для больших аналитических наборов данных.
По сути, iceberg — это прослойка между Data Lake и движками запросов, которая с помощью metadata позволяет движкам делать эффективные запросы.

философия iceberg

  • разделение data и metadata

  • ACID

Iceberg + sparkSQL

  • schema evolution и partition evolution

  • time travel и branch

философия iceberg

metadata iceberg

Metadata хранится в отдельной структуре, оптимизированной для чтения, что позволяет ускорять работу с данными без их полного переиндексирования.
Она состоит из набора таких элементов, как:

  1. metadata.json

  2. snapshot

  3. manifest list

  4. manifest file

  5. 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

Содержит ссылки на данные в рамках конкретного снимка:

  1. путь к файлу

  2. формат

  3. размер

  4. количество строк и сведения о разделах

  5. статистики по столбцам

    • min

    • max

    • null-count

    • и прочие метрики Благодаря manifest-метаданным можно проводить раннюю фильтрацию на уровне файлов, что снижает стоимость выполнения фильтров в движке запросов.

Схема чтения metadata

В момент исполнения запроса движок сначала читает текущий metadata.json -> выбирает нужный snapshot -> получает относящийся к выбранному snapshot manifest list -> подгружает manifest-файлы.

Благодаря структуре чтения metadata, Iceberg позволяет исключить наборы данных, не удовлетворяющие запросу.

1. ACID

Это ключевые гарантии, которые должна обеспечивать БД:

  1. Atomicity

  2. Consistency

  3. Isolation

  4. Durability

Atomicity

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

Consistency

Все записи и изменения схем приводят к созданию нового snapshot и, следовательно, нового manifest list.
Тем самым Iceberg переводит базу данных из одного корректного состояния в другое.

Isolation

Читатели и писатели не мешают друг другу.
И каждый query видит консистентное состояние таблицы. Iceberg реализует это через snapshot isolation.

Проблема обычного Data Lake

  1. Writer переписывает partition dt=2026-04-01/

  2. Reader в этот момент делает SELECT Он может увидеть:

    • часть старых файлов

    • часть новых

    • missing files Inconsistent table state.

Reader и Writer

  1. создаёт новые data files

  2. создаёт новый snapshot

  3. атомарно переключает 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 для тестирования фичей и командной работы, изменять схемы и партиции, не переписывая сами файлы с данными.

Комментарии (1)


  1. Strike14
    11.05.2026 17:39

    Доступное объяснение как iceberg работает с метадатой