Learning Platform
Глоссарий Troubleshooting
Урок 10.04 · 22 мин
Средний
icebergtime-travelbranchestags

Time travel: FOR VERSION AS OF, FOR TIMESTAMP AS OF, branches и tags

В прошлом уроке мы установили ключевой факт: снапшот Iceberg неизменяем, а новое изменение не правит старый снапшот, а создаёт новый. Старые снапшоты остаются жить в таблице. Из этого свойства бесплатно вытекает мощная возможность — time travel: запрос может явно обратиться к любому прошлому состоянию таблицы. В этом уроке разберём оба синтаксиса time travel в Trino, увидим, как откатить таблицу к прошлому снапшоту, и познакомимся с branches и tags — именованными указателями на снапшоты.

Откуда берётся time travel

Time travel — не отдельная «фича», которую кто-то специально встроил. Это прямое следствие архитектуры из урока 3. Каждый INSERT, UPDATE, DELETE, MERGE создаёт новый снапшот, а metadata-файл хранит список всех снапшотов таблицы. Пока снапшот не удалён процедурой expire_snapshots, все его файлы данных, манифесты и manifest list на месте и образуют согласованный, читаемый срез таблицы.

Iceberg: снапшоты, коммиты и time travel Delta Lake: Time Travel, Checkpoints и VACUUM

Обычный SELECT без указания версии читает текущий снапшот — тот, что помечен актуальным. Time travel — это просто способ сказать движку: «возьми не текущий снапшот, а вот этот конкретный». Trino поддерживает два способа адресовать снапшот: по идентификатору и по времени.

Линия снапшотов таблицы и точки обращения time travel
snapshot 1Первый INSERT, committed_at 10:00 — самое раннее доступное состояние таблицы
snapshot 2Второй INSERT, committed_at 11:00 — к этому состоянию можно обратиться по id или по timestamp
snapshot 3DELETE в 12:00 — удалил часть строк, создав новый снапшот
snapshot 4 (текущий)Последний INSERT в 13:00 — обычный SELECT без указания версии читает именно его

FOR TIMESTAMP AS OF: адресация по времени

Первый синтаксис — FOR TIMESTAMP AS OF. Он отвечает на вопрос «какой была таблица в такой-то момент». Trino находит снапшот, который был текущим на указанный момент времени: самый поздний снапшот с committed_at не позже заданного timestamp.

-- Состояние таблицы на конкретный момент в прошлом
SELECT count(*) AS orders, sum(amount) AS revenue
FROM iceberg.sales.orders
  FOR TIMESTAMP AS OF TIMESTAMP '2026-05-19 12:00:00 UTC';

--  orders | revenue
-- --------+----------
--      18 |  4250.00

Это удобно для расследований и аудита: «сколько было заказов на конец вчерашнего дня», «как выглядел отчёт до утреннего обновления». Важная тонкость: момент должен попадать в окно жизни снапшота. Если запросить timestamp раньше первого снапшота — Trino вернёт ошибку, такого состояния не существует. Если запросить timestamp в окне снапшота, который уже expired, — снапшота нет, ошибка. Time travel ограничен историей, которую таблица реально хранит.

Разберём механику разрешения timestamp точнее, потому что здесь легко ошибиться. У каждого снапшота есть момент фиксации committed_at. Снапшоты выстроены в линию по этому моменту. Запрос FOR TIMESTAMP AS OF T заставляет Trino найти снапшот, который был актуальным в момент T, — то есть последний снапшот с committed_at <= T. Если снапшот 2 зафиксирован в 11:00, а снапшот 3 — в 12:00, то любой timestamp от 11:00 включительно и до 12:00 не включительно «попадает» в снапшот 2: в этот промежуток времени именно снапшот 2 был головой таблицы. Запрос с timestamp 11:30 и запрос с timestamp 11:59 вернут идентичный результат — оба адресуют снапшот 2. Это логично: между двумя коммитами таблица не менялась, поэтому весь интервал между ними соответствует одному состоянию.

Отсюда практическое следствие: FOR TIMESTAMP AS OF отвечает на вопрос «как выглядела таблица в этот момент», а не «дай мне снапшот, созданный ровно в это время». Если нужен конкретный снапшот, а не состояние на момент, надёжнее адресовать его по идентификатору — это следующий синтаксис. Ещё одна деталь: timestamp в Trino несёт информацию о временной зоне, и при time travel это важно — TIMESTAMP '2026-05-19 12:00:00 UTC' и тот же момент в другой зоне разрешатся в один и тот же снапшот, но запись timestamp без явной зоны интерпретируется в зоне сессии, что при расследованиях через границы суток может ввести в заблуждение. В аудиторских запросах зону указывают явно.

FOR VERSION AS OF: адресация по идентификатору

Второй синтаксис — FOR VERSION AS OF. Он адресует снапшот по его точному идентификатору. Идентификаторы снапшотов видны в metadata-таблице $snapshots:

SELECT snapshot_id, committed_at, operation
FROM iceberg.sales."orders$snapshots"
ORDER BY committed_at;

--    snapshot_id      |        committed_at        | operation
-- --------------------+----------------------------+-----------
--  6841730300912934   | 2026-05-19 10:00:12.482 UTC | append
--  7720118574000021   | 2026-05-19 11:00:03.115 UTC | append
--  2287709004455187   | 2026-05-19 12:00:41.870 UTC | delete
--  9920118574521663   | 2026-05-19 13:00:55.204 UTC | append
-- Точное обращение к снапшоту по его id
SELECT count(*) AS orders
FROM iceberg.sales.orders FOR VERSION AS OF 7720118574000021;

--  orders
-- --------
--      11

FOR VERSION AS OF точнее, чем FOR TIMESTAMP AS OF: timestamp может попасть «между» двумя снапшотами и неоднозначно, а id адресует ровно один снапшот. Используйте version, когда нужна воспроизводимость: запрос с конкретным snapshot_id всегда вернёт один и тот же результат, пока этот снапшот не expired. FOR VERSION AS OF принимает не только id снапшота, но и имя ветки или тега — об этом ниже.

TIP

Практический приём отладки пайплайна. Если витрина внезапно показывает неверные числа, сравните её текущее состояние с состоянием до подозрительного запуска: SELECT * FROM mart FOR VERSION AS OF <snapshot_до> против обычного SELECT * FROM mart. Diff покажет, что именно изменил сбойный запуск, без восстановления из бэкапа.

Откат таблицы: rollback_to_snapshot

Time travel читает прошлое. Иногда нужно не просто прочитать, а вернуть таблицу в прошлое состояние — например, сбойный пайплайн записал испорченные данные. Для этого есть процедура rollback_to_snapshot.

-- Откатить таблицу к состоянию на снапшоте до сбоя
CALL iceberg.system.rollback_to_snapshot(
  schema_name   => 'sales',
  table_name    => 'orders',
  snapshot_id   => 7720118574000021
);

Важно понять механику: rollback не удаляет снапшоты, появившиеся после целевого. Он создаёт новый снапшот, чьё содержимое совпадает с целевым. История продолжает расти вперёд — просто новая «голова» таблицы указывает на данные старого снапшота. Это согласуется с принципом неизменяемости: ничего не стирается, добавляется новое состояние. Поэтому откат сам по себе обратим — можно сделать time travel к снапшоту, который был до отката.

Branches и tags: именованные указатели на снапшоты

Адресовать снапшот сырым 16-значным id неудобно и нечитаемо. Iceberg даёт именованные ссылки на снапшоты — branches (ветки) и tags (теги). Идея та же, что в Git: человекочитаемое имя вместо хеша.

Tag — неподвижный именованный указатель на один конкретный снапшот. Создали тег eom-2026-04 на снапшоте конца апреля — и он навсегда указывает на это состояние. Теги нужны для закрепления значимых точек: конец отчётного периода, состояние перед крупной миграцией, «золотой» срез для регрессионных тестов.

Branch — подвижный именованный указатель. В отличие от тега, ветка движется: запись в ветку создаёт новый снапшот, и указатель ветки переходит на него. У таблицы всегда есть ветка main — это и есть та «голова», которую читает обычный SELECT. Дополнительные ветки позволяют вести параллельные линии изменений: например, ветка staging для проверки нового пайплайна, не затрагивая main.

Tag фиксирует снапшот, branch движется при записи
tag eom-2026-04Неподвижный указатель: навсегда привязан к снапшоту конца апреля, не двигается при новых записях
указывает всегда на один снапшот
branch mainПодвижный указатель: каждая запись в ветку создаёт новый снапшот, и указатель ветки переходит на него; обычный SELECT читает main

Branches и tags создаются через ALTER TABLE ... EXECUTE:

-- Создать тег на текущем снапшоте таблицы
ALTER TABLE iceberg.sales.orders
  EXECUTE create_tag(tag_name => 'eom-2026-04');

-- Создать ветку
ALTER TABLE iceberg.sales.orders
  EXECUTE create_branch(branch_name => 'staging');

Обращаться к ветке или тегу можно через FOR VERSION AS OF с именем — читаемая альтернатива числовому id:

-- Чтение по имени тега вместо сырого snapshot_id
SELECT count(*) AS orders_at_eom
FROM iceberg.sales.orders FOR VERSION AS OF 'eom-2026-04';

--  orders_at_eom
-- ---------------
--           1240

Все ветки и теги таблицы видны в metadata-таблице $refs:

SELECT name, type, snapshot_id
FROM iceberg.sales."orders$refs";

--      name      | type   |   snapshot_id
-- ---------------+--------+------------------
--  main          | BRANCH | 9920118574521663
--  staging       | BRANCH | 9920118574521663
--  eom-2026-04   | TAG    | 6841730300912934

Зачем ветки нужны на практике

Branches — это не «фича на всякий случай», у них есть конкретные инженерные применения, и понимать их полезно даже на middle-уровне.

Первое — write-audit-publish, паттерн «записать, проверить, опубликовать». Прямая запись в боевую таблицу main означает, что некачественные данные сразу видны всем потребителям отчётов. Ветка разрывает этот риск. Пайплайн пишет новую порцию данных не в main, а в отдельную ветку, например audit. Пока данные в ветке, на ней прогоняются проверки качества — обычными SQL-запросами к этой ветке. Только если проверки прошли, изменения переносят в main. Потребители всё это время видят прежний, проверенный main; брак до них не доходит. Это перенос идеи «не мержить непроверенный код» из разработки на данные.

Второе — изоляция экспериментов. Нужно проверить, как на таблицу повлияет крупная переработка пайплайна, но без риска для боевых данных. Ветка experiment даёт песочницу: вы прогоняете новую логику в ветке, сравниваете результат с main, и если эксперимент неудачен — просто отбрасываете ветку, main не затронут.

Принципиальное отличие ветки от тега ещё раз: тег фиксирует прошлое (закрепляет состояние, которое уже было), ветка ведёт параллельное настоящее (отдельную живую линию изменений). Тег — это закладка в истории, ветка — это альтернативная голова.

TIP

Запись в конкретную ветку и продвинутые операции с ветками (например, перенос изменений между ветками) в экосистеме Iceberg часто выполняются через Spark-процедуры, а полнота поддержки веток на запись в Trino зависит от версии. Для аналитика на Trino важнее уверенно владеть чтением: FOR VERSION AS OF 'имя-ветки-или-тега' и таблицей $refs. Перед тем как строить write-audit-publish именно на Trino, сверьтесь с release notes вашей версии — какие операции с ветками доступны.

WARNING

Time travel и ветки работают только пока снапшот жив. Процедура expire_snapshots (следующий урок) удаляет старые снапшоты и их файлы — после этого обратиться к ним нельзя. Если снапшот закреплён тегом или является головой ветки, expire его не тронет: ref защищает снапшот от удаления. Поэтому тег — это ещё и способ гарантировать, что важное состояние переживёт обслуживание таблицы.

Попробуй сам

В песочнице создайте iceberg.sales.orders и сделайте серию изменений с паузами: INSERT, затем INSERT, затем DELETE части строк, затем ещё INSERT. Откройте orders$snapshots, выпишите все snapshot_id и committed_at. Теперь упражнения. Первое: выполните SELECT count(*) с FOR VERSION AS OF для каждого снапшота и проследите, как менялось число строк по истории. Второе: возьмите committed_at второго снапшота, выполните FOR TIMESTAMP AS OF чуть позже этого времени и убедитесь, что результат совпал с FOR VERSION AS OF второго снапшота. Третье: создайте тег before-delete на снапшоте, который был перед DELETE, затем выполните rollback_to_snapshot к этому снапшоту. Проверьте orders$snapshots — сколько теперь снапшотов и почему их число выросло, а не уменьшилось. Объясните письменно, почему откат не стирает историю.


Проверка знанийKnowledge check
Чем отличаются branch и tag в Iceberg, и почему откат таблицы через rollback_to_snapshot не уменьшает, а увеличивает число снапшотов?
ОтветAnswer
Branch и tag — это именованные человекочитаемые указатели на снапшоты, аналог веток и тегов в Git. Разница в подвижности. Tag — неподвижный указатель на один конкретный снапшот: создав тег, вы навсегда закрепляете за именем это состояние, новые записи тег не двигают. Теги нужны для фиксации значимых точек — конец отчётного периода, состояние перед миграцией, золотой срез для тестов. Branch — подвижный указатель: запись в ветку создаёт новый снапшот, и указатель ветки переходит на него. У таблицы всегда есть ветка main — её читает обычный SELECT; дополнительные ветки позволяют вести параллельные линии изменений. Откат через rollback_to_snapshot увеличивает число снапшотов, потому что он подчиняется принципу неизменяемости: снапшоты Iceberg никогда не меняются и не стираются по месту. Rollback не удаляет снапшоты, появившиеся после целевого — он создаёт новый снапшот, чьё содержимое совпадает с целевым, и делает его новой головой таблицы. История продолжает расти вперёд, просто новая голова указывает на данные старого состояния. Благодаря этому сам откат обратим: можно сделать time travel к снапшоту, который был до отката. Дополнительно, ветка или тег защищают свой снапшот от удаления процедурой expire_snapshots, поэтому тег гарантирует, что важное состояние переживёт обслуживание таблицы.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чем FOR VERSION AS OF отличается от FOR TIMESTAMP AS OF при time travel в Trino?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7