Delta-процедуры: OPTIMIZE, VACUUM, register_table
Delta-таблица, как и Iceberg, — живой объект: она деградирует и требует обслуживания. Проблемы те же — мелкие файлы и накопленная история, — но процедуры Delta называются иначе и в деталях работают по-своему. Этот урок разбирает три ключевые процедуры: OPTIMIZE для компакции, VACUUM для уборки старых файлов и register_table для подключения существующих Delta-таблиц. Опираясь на модуль по Iceberg, мы будем постоянно сравнивать — это лучший способ закрепить, что разные форматы решают одни задачи.
Почему Delta-таблица деградирует
Деградация Delta вытекает из тех же свойств, что у Iceberg, через призму transaction log из прошлого урока.
Мелкие файлы. Каждый INSERT добавляет новые Parquet-файлы и пишет в transaction log действие add. Дописать в существующий файл нельзя — файлы данных неизменяемы. Частые мелкие INSERT и стриминг плодят тысячи маленьких файлов. На чтении это бьёт так же, как у Iceberg: больше файлов — больше сплитов, больше накладных расходов на открытие файлов и чтение Parquet-footer.
Накопление мёртвых файлов. Когда UPDATE, DELETE или OPTIMIZE переписывают файл, в transaction log пишется remove для старого файла и add для нового. Но remove в журнале не означает физического удаления файла с диска — он лишь помечает файл как не входящий в текущее состояние таблицы. Старый Parquet-файл остаётся в object storage. Зачем? Ровно для time travel: запрос FOR VERSION AS OF к прошлой версии должен прочитать файлы, которые тогда были живыми. Пока эти версии достижимы, физически удалять старые файлы нельзя.
Итог: со временем в директории Delta-таблицы скапливаются и мелкие файлы (вредят чтению), и мёртвые файлы — те, что помечены remove и нужны только истории (вредят счёту за storage). Две оси деградации, как у Iceberg.
OPTIMIZE: компакция мелких файлов
OPTIMIZE в Delta решает проблему мелких файлов — ровно как одноимённая операция в Iceberg. Процедура читает мелкие файлы данных и переписывает их содержимое в меньшее число крупных файлов. В transaction log это фиксируется как новая транзакция: remove для мелких файлов, add для крупных. Данные логически не меняются — те же строки, эффективно упакованные.
В Trino компакция Delta вызывается через ALTER TABLE ... EXECUTE optimize:
-- Скомпактить мелкие файлы Delta-таблицы в крупные
ALTER TABLE delta.ops.accounts EXECUTE optimize;
-- Компактить только файлы меньше заданного размера
ALTER TABLE delta.ops.accounts
EXECUTE optimize(file_size_threshold => '128MB');
Как и у Iceberg, на больших таблицах разумно ограничивать область компакции — деградирует обычно свежая партиция, в которую льются данные. OPTIMIZE принимает WHERE по партициям:
-- Компактить только вчерашнюю партицию
ALTER TABLE delta.ops.events
EXECUTE optimize
WHERE event_date = DATE '2026-05-19';
Ключевое, что нужно перенести из модуля по Iceberg: OPTIMIZE не освобождает место. Он создал новые крупные файлы, а мелкие пометил remove в журнале — но физически они остались в object storage, потому что нужны для time travel к прошлым версиям. После OPTIMIZE директория таблицы временно даже выросла. Чтение ускорилось, место не освободилось. Удалить мёртвые файлы — задача VACUUM.
VACUUM: физическое удаление мёртвых файлов
VACUUM физически удаляет из object storage файлы, которые помечены remove в transaction log и больше не нужны. Это аналог expire_snapshots из мира Iceberg — процедура, реально освобождающая место.
-- Удалить мёртвые файлы старше retention по умолчанию
ALTER TABLE delta.ops.accounts EXECUTE vacuum;
-- Явно задать порог retention
ALTER TABLE delta.ops.accounts
EXECUTE vacuum(retention => '7d');
Здесь — критически важный момент, прямая параллель с минимальным retention у expire_snapshots. У VACUUM есть retention-порог (по умолчанию 7 дней), и понижать его опасно. Причина двойная.
Первая: VACUUM физически уничтожает файлы. Удалив мёртвый файл, вы навсегда теряете возможность time travel к версиям, которые на него ссылались. После VACUUM с порогом 7 дней time travel дальше 7 дней назад перестаёт работать — соответствующих файлов больше нет. Retention VACUUM — это и есть фактическая глубина вашего time travel.
Вторая, ещё опаснее: мёртвый файл и файл недавней или идущей прямо сейчас транзакции по виду в директории неотличимы. Слишком короткий retention рискует удалить файл операции, которая ещё не успела завершиться или на которую опирается параллельный читатель. Поэтому Delta защищает свежие файлы порогом и предупреждает при попытке его занизить.
VACUUM необратим: удалённые файлы данных не восстановить. Перед запуском VACUUM с уменьшенным retention убедитесь, что более глубокий time travel вам действительно не нужен — после удаления файлов вернуть прошлые версии будет неоткуда. Retention VACUUM напрямую задаёт, насколько далеко в прошлое работает FOR VERSION AS OF и FOR TIMESTAMP AS OF.
Порядок процедур такой же, как у Iceberg: сначала OPTIMIZE (компакция, старые файлы становятся мёртвыми), затем VACUUM (физическая уборка мёртвых файлов). OPTIMIZE без последующего VACUUM ускоряет чтение, но не уменьшает счёт за storage.
Сравнение терминов с Iceberg, чтобы не путать:
| Задача | Iceberg | Delta Lake |
|---|---|---|
| Компакция мелких файлов | optimize | optimize |
| Физическое удаление старого / освобождение места | expire_snapshots | vacuum |
| Уборка файлов-сирот вне метаданных | remove_orphan_files | (частично покрывается vacuum) |
| Что задаёт глубину time travel | retention expire_snapshots | retention vacuum |
register_table: подключение существующей Delta-таблицы
Последняя процедура — register_table. Она решает не деградацию, а онбординг: как сделать видимой в Trino Delta-таблицу, которая уже существует в object storage, но не зарегистрирована в metastore вашего каталога.
Сценарий типичный. Spark-пайплайн или платформа Databricks создали Delta-таблицу: в object storage есть директория данных и _delta_log/. Сама таблица полностью самодостаточна — transaction log внутри неё описывает всё. Но metastore вашего каталога Trino про эту таблицу не знает: для него нет записи «имя -> директория». register_table создаёт эту запись.
-- Зарегистрировать существующую Delta-таблицу в каталоге Trino.
-- Требует delta.register-table-procedure.enabled=true в каталоге.
CALL delta.system.register_table(
schema_name => 'ops',
table_name => 'accounts_external',
table_location => 's3://lake/external/accounts'
);
Процедура не трогает данные и не трогает _delta_log/ — она лишь добавляет в metastore указатель на директорию. После этого SELECT FROM delta.ops.accounts_external работает: коннектор по указателю находит директорию, читает _delta_log/ и видит таблицу целиком. Парная процедура unregister_table убирает указатель из metastore, тоже не удаляя данные.
Важная деталь безопасности: register_table по умолчанию выключена и требует явного delta.register-table-procedure.enabled=true в файле каталога. Причина — регистрация по произвольному пути потенциально опасна, и администратор должен включить эту возможность сознательно.
register_table — основной инструмент миграции на Trino без копирования данных. Если организация переходит с Spark-only на Trino как движок запросов, существующие Delta-таблицы не нужно переливать: достаточно зарегистрировать их по их путям. Данные остаются на месте, Trino начинает их читать. Это прямая выгода открытого формата таблиц — таблица не привязана к движку.
Delta-обслуживание глазами transaction log
Свяжем процедуры обслуживания с моделью из прошлого урока — это даёт более глубокое понимание, чем заучивание имён процедур.
Вспомните: состояние Delta-таблицы — это свёртка transaction log. Каждый OPTIMIZE, DELETE, VACUUM — это новая транзакция, новый пронумерованный JSON-файл в _delta_log/. Отсюда первое наблюдение: обслуживание само по себе удлиняет журнал. OPTIMIZE не «убирает записи из журнала», он дописывает в него транзакцию с действиями remove и add. Журнал монотонно растёт.
Это объясняет роль checkpoint. Если бы журнал только рос, чтение состояния со временем требовало бы проигрывать всё больше JSON-файлов. Checkpoint — Parquet-файл со свёрнутым состоянием — периодически создаётся, чтобы движок доигрывал лишь хвост журнала после последнего checkpoint. Checkpoint к обслуживанию данных прямого отношения не имеет, но это часть «гигиены» Delta-таблицы: он держит стоимость чтения метаданных ограниченной независимо от того, сколько транзакций накопилось.
И ещё одно следствие. Когда VACUUM физически удаляет мёртвый файл данных, запись remove об этом файле в старых транзакциях журнала остаётся — журнал неизменяем, в нём нельзя стереть прошлое. Просто time travel к версии, которая ссылалась на удалённый файл, перестаёт работать: журнал помнит, что файл был, но самого файла на диске уже нет. Это согласуется с тем, что VACUUM необратим и его retention задаёт фактическую глубину time travel. Понимание через transaction log делает поведение процедур не набором правил, а следствием одной модели.
Попробуй сам
В песочнице с каталогом delta создайте таблицу events. Упражнение первое: сымитируйте стриминг — выполните 30-40 мелких INSERT, снимите метрику числа файлов (посмотрите директорию таблицы в MinIO или метаданные). Выполните ALTER TABLE EXECUTE optimize, снова посмотрите директорию — обратите внимание, что число файлов в _delta_log выросло, а физических Parquet-файлов стало не меньше, а больше. Объясните, почему OPTIMIZE не уменьшил число файлов на диске. Упражнение второе: выполните ALTER TABLE EXECUTE vacuum и снова посмотрите директорию — теперь мёртвые файлы убраны. Письменно ответьте: почему важен порядок OPTIMIZE затем VACUUM, и как retention у VACUUM связан с тем, насколько далеко назад работает time travel. Упражнение третье на register_table: возьмите путь существующей Delta-таблицы (или создайте таблицу, затем мысленно «забудьте» её регистрацию) и зарегистрируйте её под новым именем через register_table; убедитесь, что SELECT работает, и объясните, почему процедура не копировала данные.