Metadata-таблицы, Iceberg v2 и v3, тип VARIANT, материализованные представления
Завершаем модуль по Iceberg тремя темами, которые делают его инструментом эксплуатации, а не только хранения. Metadata-таблицы — окно в дерево метаданных из урока 3, через которое инженер диагностирует таблицу обычным SQL. Версии формата (v1, v2, v3) определяют, что таблица вообще умеет — от row-level DELETE до нового типа VARIANT. И материализованные представления — способ ускорить тяжёлые запросы поверх lakehouse. Этот урок собирает их вместе и даёт инструментарий для повседневной работы с Iceberg.
Metadata-таблицы: дерево метаданных как обычные таблицы
В уроке 3 мы разобрали дерево метаданных: metadata-файл, снапшоты, manifest list, манифесты, файлы данных. Iceberg-коннектор Trino даёт доступ к каждому уровню этого дерева через metadata-таблицы — виртуальные таблицы, к которым обращаются, дописав к имени таблицы суффикс $имя. Это не отдельные объекты в storage, а представление метаданных в табличном виде. Их можно фильтровать, джойнить, агрегировать — обычным SQL.
Основные metadata-таблицы:
| Таблица | Что показывает | Когда нужна |
|---|---|---|
$snapshots | Все снапшоты: id, время, операция, summary | История изменений, выбор снапшота для time travel |
$history | Линия родитель-потомок снапшотов | Понять, как развивалась таблица, найти ветвления |
$files | Каждый файл данных: путь, число строк, размер, статистика | Диагностика мелких файлов, анализ pruning |
$manifests | Манифесты текущего снапшота | Оценить, не пора ли optimize_manifests |
$partitions | Сводка по партициям: число файлов и строк в каждой | Найти перекошенные или мелкие партиции |
$refs | Ветки и теги таблицы | Аудит именованных указателей |
$properties | Свойства таблицы | Проверить настройки формата, retention |
Имя metadata-таблицы содержит $, поэтому в Trino его берут в двойные кавычки:
-- Здоровье таблицы одним запросом: распределение файлов по партициям
SELECT partition,
record_count,
file_count,
file_count > 50 AS too_many_files
FROM iceberg.sales."events$partitions"
ORDER BY file_count DESC;
-- partition | record_count | file_count | too_many_files
-- -------------------+--------------+------------+----------------
-- {order_day: 5-19} | 2950000 | 1843 | true
-- {order_day: 5-20} | 120000 | 12 | false
Этот запрос мгновенно показывает: партиция за 19 мая деградировала (1843 файла) и просит OPTIMIZE, остальные здоровы. То, что в обычной БД потребовало бы лезть в системные представления движка, в Iceberg делается переносимым SQL поверх metadata-таблиц.
-- Найти мелкие файлы по всей таблице
SELECT count(*) AS small_files,
sum(record_count) AS rows_in_small_files
FROM iceberg.sales."events$files"
WHERE file_size_in_bytes < 8 * 1024 * 1024;
-- small_files | rows_in_small_files
-- -------------+---------------------
-- 1791 | 2410000
Metadata-таблицы — основа автоматизации обслуживания. Пайплайн обслуживания не запускает OPTIMIZE вслепую по расписанию, а сначала запросом к $partitions или $files проверяет, есть ли деградация, и компактит только те партиции, где она реально есть. Это экономит дорогой I/O компакции.
Версии формата: v1, v2, v3
У формата Iceberg есть номер версии спецификации, и он определяет, что таблица умеет. Версия задаётся свойством format_version при создании.
v1 — базовая версия. Таблица — это набор файлов данных, снапшоты, партиционирование. v1 умеет append (добавление данных) и перезапись на уровне файлов, но не умеет эффективный row-level DELETE. Сегодня v1 — фактически legacy.
v2 — версия по умолчанию в актуальном Trino. Главное нововведение — delete files. Они позволяют удалить отдельные строки, не переписывая весь файл данных. Вспомните: файлы данных неизменяемы. Как тогда удалить одну строку из файла на миллион строк? v2 пишет рядом небольшой delete file, который говорит «в таком-то файле строка с такой-то позицией удалена». При чтении движок применяет delete files поверх данных. Именно delete files делают возможными DELETE, UPDATE и MERGE на уровне строк без переписывания крупных файлов. Для большинства задач v2 — правильный выбор.
v3 — новейшая версия, экспериментальная в актуальном Trino. Её стоит знать, но в продакшене применять осознанно, понимая статус «экспериментально». v3 приносит набор возможностей:
- Тип VARIANT — о нём отдельно ниже; это флагманская фича v3, и VARIANT доступен только в v3.
- Binary deletion vectors — более эффективное представление удалённых строк, чем delete files v2: компактный битмап позиций вместо отдельных файлов.
- Column default values — значения по умолчанию для колонок на уровне формата.
- Nanosecond timestamps — временные метки наносекундной точности.
- Row lineage — отслеживание происхождения строк между снапшотами.
-- v2 (по умолчанию): row-level DELETE через delete files,
-- крупные файлы данных не переписываются
CREATE TABLE iceberg.sales.accounts (
account_id BIGINT,
status VARCHAR,
updated_at TIMESTAMP(6)
) WITH (format = 'PARQUET', format_version = 2);
DELETE FROM iceberg.sales.accounts WHERE status = 'closed';
-- DELETE: 37 rows
-- v3: создаётся осознанно, когда нужен VARIANT или другие фичи v3
CREATE TABLE iceberg.sales.events_v3 (
event_id BIGINT,
payload VARCHAR
) WITH (format = 'PARQUET', format_version = 3);
Тип VARIANT: полуструктурированные данные в колонке
VARIANT — флагманский тип Iceberg v3 (в Trino — экспериментальный, как и сам v3). Он решает реальную проблему дата-инженерии: данные, у которых нет фиксированной схемы.
Событийные логи, payload вебхуков, ответы внешних API — у каждой записи свой набор полей, и схема меняется со временем без предупреждения. Загнать такое в строгие колонки нельзя: одно событие имеет поле device.os, другое — нет, третье добавляет вложенный массив. Раньше выбор был между двумя плохими вариантами. Хранить как VARCHAR с JSON-текстом — тогда движок не понимает структуру, каждый доступ к полю парсит строку заново, фильтры по полям не оптимизируются. Либо завести колонку под каждое возможное поле — но полей сотни, они меняются, схема превращается в свалку.
VARIANT — третий путь. Это тип для полуструктурированных данных: внутри одной колонки хранится произвольная вложенная структура — объекты, массивы, скаляры — но хранится в эффективном бинарном представлении, а не как текст. Движок понимает структуру VARIANT-значения, умеет доставать вложенные поля и не парсит строку на каждое обращение. Это золотая середина между «слепым текстом» и «жёсткими колонками».
-- VARIANT доступен только в Iceberg v3 (экспериментально в Trino)
CREATE TABLE iceberg.sales.events_var (
event_id BIGINT,
props VARIANT
) WITH (format = 'PARQUET', format_version = 3);
-- В одну VARIANT-колонку ложатся записи с разным набором полей
INSERT INTO iceberg.sales.events_var VALUES
(1, CAST(JSON '{"device":{"os":"iOS"},"premium":true}' AS VARIANT)),
(2, CAST(JSON '{"device":{"os":"Android"},"referrer":"ads"}' AS VARIANT));
-- INSERT: 2 rows
Apache Iceberg v3: тип VARIANT на уровне формата
VARIANT не отменяет обычные колонки. Стабильные, всегда присутствующие поля (event_id, user_id, event_ts) держат строгими типизированными колонками — по ним идёт партиционирование, статистика, pruning. А «хвост» из изменчивых, разреженных, необязательных атрибутов уходит в одну VARIANT-колонку. Это типовой паттерн событийных таблиц в lakehouse 2026 года.
Почему именно такое разделение, а не «всё в VARIANT»? Потому что у строгой типизированной колонки и у поля внутри VARIANT разная цена доступа. По строгой колонке Iceberg ведёт статистику min/max в манифестах — значит, по ней работает file pruning и партиционирование. Поле внутри VARIANT движок видит, но это полуструктурированное значение, и отсечь файлы по нему так же дёшево, как по обычной колонке, не всегда возможно. Отсюда правило: всё, по чему вы фильтруете, партиционируете и джойните, должно быть строгой колонкой; в VARIANT уходит то, что вы в основном просто читаете и достаёте по необходимости. Неправильное разделение — например, ключ join, спрятанный в VARIANT, — лишит запрос оптимизаций так же, как и в случае со слепым JSON-текстом.
Стоит также отметить эволюционную связь возможностей v3. Тип VARIANT, binary deletion vectors, row lineage — это не разрозненный набор, а согласованное движение Iceberg к зрелости: deletion vectors делают row-level DML эффективнее, row lineage — отслеживаемее, VARIANT — расширяет применимость на полуструктурированные данные. Все они приходят вместе в v3 именно потому, что v3 — это следующий большой шаг спецификации. Для дата-инженера практический смысл прост: v3 стоит держать на радаре, отслеживать в release notes Trino степень его готовности, но строить продакшен сегодня на проверенном v2.
VARIANT и весь Iceberg v3 в актуальном Trino — экспериментальные. Для учебных задач и пилотов это нормально, но прежде чем строить продакшен на v3, сверьтесь с release notes вашей версии Trino: набор поддержанных операций над VARIANT расширяется от релиза к релизу. Стабильный, проверенный выбор для продакшена сегодня — v2.
Материализованные представления над lakehouse
Iceberg-коннектор поддерживает материализованные представления (materialized views). Обычный VIEW — это сохранённый текст запроса: каждое обращение к нему заново выполняет весь запрос. Материализованное представление хранит результат запроса как настоящую Iceberg-таблицу.
Зачем это в lakehouse. Тяжёлый запрос — join нескольких больших таблиц с агрегацией — может идти минуты. Если он стоит за дашбордом, который открывают сто раз в день, пересчитывать его каждый раз расточительно. Материализованное представление считает результат один раз, сохраняет в Iceberg-таблицу, и обращения читают готовый результат — быстро.
-- Тяжёлая агрегация считается один раз и хранится как Iceberg-таблица
CREATE MATERIALIZED VIEW iceberg.sales.mv_daily_revenue AS
SELECT date(order_ts) AS order_day,
count(*) AS orders,
sum(amount) AS revenue
FROM iceberg.sales.events
GROUP BY date(order_ts);
-- Обновить сохранённый результат, когда исходные данные изменились
REFRESH MATERIALIZED VIEW iceberg.sales.mv_daily_revenue;
-- Обращение читает готовый результат, а не пересчитывает join и агрегацию
SELECT * FROM iceberg.sales.mv_daily_revenue
WHERE order_day = DATE '2026-05-19';
Ключевая ответственность инженера — свежесть. Материализованное представление не обновляется само: после изменения исходных таблиц нужно REFRESH, иначе представление отдаёт устаревший результат. Преимущество Iceberg-реализации в том, что REFRESH может быть инкрементальным — благодаря снапшотам Iceberg знает, что изменилось с прошлого обновления, и пересчитывает не всё, а только дельту. На больших таблицах это разница между секундами и минутами. REFRESH обычно ставят в тот же пайплайн обслуживания, что и OPTIMIZE с expire_snapshots из урока 5.
Попробуй сам
В песочнице возьмите Iceberg-таблицу из прошлых уроков. Упражнение первое, metadata-таблицы: напишите один SQL-запрос к $partitions, который выводит партиции, отсортированные по числу файлов, и помечает флагом те, где файлов больше 50 — это ваш «детектор деградации». Запросом к $snapshots посчитайте, снапшоты каких операций (append, delete, overwrite) встречаются в истории и сколько каждого. Упражнение второе, версии: создайте две таблицы — с format_version = 2 и format_version = 3 — выполните DELETE нескольких строк в каждой и сравните, посмотрев $files и $snapshots. Упражнение третье, материализованное представление: создайте MV с агрегацией поверх таблицы, выполните SELECT из него, затем вставьте новые строки в исходную таблицу и снова сделайте SELECT из MV — изменился ли результат? Выполните REFRESH и проверьте снова. Письменно объясните, почему MV не обновился сам и в чём преимущество инкрементального REFRESH в Iceberg.