Iceberg connector: каталоги, метаданные, snapshots
Мы выяснили: lakehouse-таблица — это файлы данных плюс слой метаданных, а каталог хранит указатель на текущую версию метаданных. Теперь заглянем внутрь самого слоя метаданных. Что конкретно лежит в metadata-файле? Что такое snapshot и manifest? И как координатор Trino, прочитав это дерево, понимает, какие именно Parquet-файлы и в каком порядке отдать воркерам? Без этой картины невозможно осмысленно делать ни time travel, ни обслуживание таблиц, ни pruning — поэтому этот урок разбирает анатомию Iceberg-таблицы по слоям.
Трёхуровневое дерево метаданных
Метаданные Iceberg-таблицы — это не один файл, а дерево из трёх уровней. Каждый уровень — отдельные файлы в том же object storage, что и данные. Идти по дереву нужно сверху вниз, и Trino именно так его и читает.
Уровень 1: metadata-файл. На него указывает каталог. Это JSON-файл (vNNN.metadata.json). Он хранит описание таблицы целиком: текущую схему (и историю прошлых схем), спецификацию партиционирования, свойства таблицы и — главное — список всех snapshot’ов и указание, какой из них текущий. Каждое изменение таблицы создаёт новый metadata-файл с новым набором снапшотов.
Уровень 2: snapshot и manifest list. Snapshot — это состояние таблицы на один момент времени, то есть полный набор файлов данных в этот момент. Сам snapshot не перечисляет файлы данных напрямую — он указывает на manifest list: один файl (в формате Avro), который перечисляет манифесты этого снапшота. Manifest list для каждого манифеста хранит сводную статистику — диапазоны значений партиционирующих колонок. Это позволяет на уровне списка отбросить целые манифесты, не открывая их.
Уровень 3: manifest. Manifest — тоже Avro-файл. Он перечисляет конкретные файлы данных (Parquet/ORC): для каждого файла — путь, число строк, размер, к какой партиции относится и, критически важно, статистику по колонкам: min/max значений, доля null, количество значений. Manifest также помечает каждый файл как добавленный, существующий или удалённый — это нужно для инкрементального чтения и для delete-файлов.
Зачем три уровня, а не один список файлов? Потому что у зрелой таблицы миллионы файлов данных. Хранить их одним списком — значит читать гигабайты метаданных на каждый запрос. Дерево позволяет читать метаданные выборочно: по manifest list отбросить манифесты ненужных партиций, не открывая их; внутри оставшихся манифестов по статистике отбросить ненужные файлы данных. Метаданные читаются почти так же избирательно, как данные. Это и есть инженерное ядро Iceberg.
Iceberg: снапшоты, коммиты и time travel Iceberg: hidden partitioning и partition evolutionSnapshot — это immutable срез таблицы
Ключевое свойство снапшота — он неизменяемый. Раз созданный, snapshot никогда не меняется: он навсегда описывает один набор файлов. Изменение таблицы не правит существующий snapshot — оно создаёт новый.
Разберём, что происходит при INSERT. Воркеры пишут новые Parquet-файлы. Создаётся новый манифест, перечисляющий их. Создаётся новый manifest list — он включает и новый манифест, и манифесты из предыдущего снапшота (старые файлы никуда не делись). Создаётся новый snapshot, указывающий на этот manifest list. Создаётся новый metadata-файл, добавляющий этот snapshot в список и помечающий его текущим. Финал — каталог атомарно переключает указатель.
Предыдущий snapshot при этом остаётся жив и валиден. Все его файлы данных на месте, его manifest list и манифесты на месте. Запрос, начавшийся до коммита, продолжает читать старый snapshot и видит согласованные данные. Это даёт три вещи сразу: атомарность (новый snapshot виден весь или никак), snapshot isolation для конкурентных запросов и — бонусом — time travel: раз старые снапшоты не удаляются, к ним можно обратиться явно. Этому посвящён следующий урок.
Старые снапшоты живут не вечно — иначе таблица копила бы все файлы за всю историю. Их удаляет процедура expire_snapshots из урока про обслуживание таблиц. До тех пор пока снапшот не expired, его данные доступны для чтения и time travel. Это сознательный компромисс: история занимает место в object storage, и инженер сам решает, сколько её хранить.
Как Trino читает Iceberg-таблицу
Соберём всё в сквозной путь запроса SELECT ... FROM iceberg.sales.orders WHERE order_date = DATE '2026-05-01'.
Сначала координатор спрашивает каталог: где текущий metadata-файл sales.orders? Получает путь, читает JSON, находит текущий snapshot. Из снапшота берёт manifest list и читает его. Здесь срабатывает первый отсев: manifest list хранит диапазоны партиционирующих колонок по каждому манифесту — манифесты, чьи диапазоны заведомо не пересекаются с order_date = '2026-05-01', отбрасываются целиком, не открываясь.
Затем координатор читает оставшиеся манифесты. В них — список файлов данных со статистикой min/max по колонкам. Здесь второй отсев: файл, у которого min/max order_date не покрывает '2026-05-01', пропускается. Это и есть file pruning на основе статистики из манифестов. По выжившим файлам координатор генерирует сплиты — обычно сплит соответствует файлу или его части (row group). Воркеры читают эти Parquet-файлы из object storage параллельно.
Главный вывод: чем больше отсеялось на уровне метаданных, тем меньше Parquet Trino реально откроет. Поэтому статистика в манифестах — не «приятное дополнение», а механизм, напрямую определяющий объём I/O. Партиционирование, которое мы разберём в уроке 6, работает именно через эти диапазоны в manifest list.
Настройка коннектора и проверка на практике
Файл каталога Trino для Iceberg задаёт коннектор, тип каталога метаданных и доступ к object storage.
# etc/catalog/iceberg.properties
connector.name=iceberg
iceberg.catalog.type=rest
iceberg.rest-catalog.uri=http://rest-catalog:8181
iceberg.rest-catalog.warehouse=s3://lake/warehouse
fs.native-s3.enabled=true
s3.endpoint=http://minio:9000
s3.path-style-access=true
# Формат файлов по умолчанию для новых таблиц
iceberg.file-format=PARQUET
Создадим таблицу и сделаем два INSERT — каждый создаёт snapshot:
CREATE TABLE iceberg.sales.orders (
order_id BIGINT,
order_date DATE,
amount DECIMAL(12,2)
) WITH (format = 'PARQUET');
INSERT INTO iceberg.sales.orders VALUES
(1, DATE '2026-05-01', DECIMAL '250.00'),
(2, DATE '2026-05-01', DECIMAL '99.90');
-- INSERT: 2 rows
INSERT INTO iceberg.sales.orders VALUES
(3, DATE '2026-05-02', DECIMAL '12.50');
-- INSERT: 1 row
Дерево метаданных видно через системные metadata-таблицы — суффикс $ к имени таблицы. Полный их разбор — в уроке 7, но snapshots посмотрим уже сейчас, два INSERT дали два снапшота:
SELECT snapshot_id, operation,
summary['added-records'] AS added_records
FROM iceberg.sales."orders$snapshots"
ORDER BY committed_at;
-- snapshot_id | operation | added_records
-- -------------------+-----------+---------------
-- 6841...300912934 | append | 2
-- 9920...118574521 | append | 1
Таблица $files показывает уровень 3 дерева — конкретные файлы данных и их статистику, ту самую, по которой идёт pruning:
SELECT file_path, record_count, file_size_in_bytes
FROM iceberg.sales."orders$files";
-- file_path | record_count | file_size_in_bytes
-- -----------------------------------------+--------------+--------------------
-- s3://lake/.../data/00000-...-abc.parquet | 2 | 714
-- s3://lake/.../data/00000-...-def.parquet | 1 | 655
Два файла данных — по одному на INSERT. Каждый INSERT не дописывает в существующий файл, а создаёт новый: файлы данных в Iceberg тоже неизменяемы. Это объясняет, почему частые мелкие INSERT плодят мелкие файлы и зачем нужна компакция (OPTIMIZE) — тема урока 5.
Почему дерево метаданных — это не лишний слой
У начинающих возникает разумное возражение: трёхуровневое дерево метаданных — это же дополнительная работа, не замедляет ли оно запросы по сравнению с «просто папкой файлов»? Ответ — нет, и понимать почему важно.
Сравним стоимость. У data lake без формата таблиц «определение списка файлов» — это операция list по директории. На таблице с миллионом файлов list возвращает миллион записей, и это всё, что есть, — никакой статистики, чтобы их отсеять. Дальше движок вынужден открывать файлы и читать их footer, чтобы понять, нужны ли они. У Iceberg «определение списка файлов» — это чтение дерева метаданных, и оно устроено так, чтобы прочитать меньше: manifest list компактен и сразу отсекает целые манифесты, а в манифесте статистика лежит рядом со списком файлов, так что отсев файла не требует открывать сам файл. Iceberg добавляет слой метаданных, но этот слой спроектирован так, чтобы суммарно читать меньше, а не больше.
Есть и второй выигрыш — точность. Координатор Trino, прочитав манифесты, знает точное число файлов, их размеры и статистику ещё до генерации сплитов. Это даёт ему основу и для аккуратного планирования (сколько сплитов создавать, как их распределить), и для cost-based optimizer (число строк, min/max — это и есть статистика для оценки кардинальности). У «папки файлов» этой информации нет заранее, поэтому и планировать, и оптимизировать запрос движок вынужден почти вслепую. Дерево метаданных — это не накладной расход, а то, что одновременно ускоряет чтение и делает возможной осмысленную оптимизацию.
Попробуй сам
Поднимите песочницу курса (Trino + REST catalog + MinIO) и воспроизведите пример: создайте iceberg.sales.orders, сделайте три отдельных INSERT. Затем исследуйте дерево метаданных. Посмотрите orders$snapshots — сколько снапшотов, какой operation у каждого. Посмотрите orders$files — сколько файлов данных и почему столько. Посмотрите orders$manifests — сколько манифестов. Теперь главный вопрос на понимание: выполните SELECT * FROM orders$files и найдите в выводе колонки со статистикой по колонкам данных (нижние и верхние границы значений). Объясните своими словами, как координатор Trino использовал бы эти границы, если бы вы выполнили SELECT * FROM orders WHERE order_date = DATE '2026-05-02' — какие файлы он открыл бы, а какие пропустил и на основании чего.