14. タイムトラベルと branch / tag
Iceberg のテーブルは、コミットのたびにスナップショットを積み上げます。古いスナップショットはコミット後も残り続けるため、過去の任意の時点の状態をそのまま読めます。この仕組みの上に、名前付き参照である branch と tag が乗っています。
ここでは samples/04_time_travel.py を使い、3世代のスナップショットを作ってから、ID 指定の読み取り、tag と branch の作成、ロールバックまでを一通り実行します。あわせて、tag を打つとスナップショットの寿命が延びるという運用上の副作用も確認します。
確かめたいこと
- スナップショット ID を指定すると過去の状態が読めること
- tag と branch がテーブルの refs にどう現れるか
- tag / branch から参照されたスナップショットは expire の対象外になること
- ロールバック後、捨てられたコミットが履歴上どう見えるか
背景
Iceberg のテーブルメタデータは refs というマップを持ち、名前を snapshot ID に対応づけます。branch は移動する参照、tag は固定された参照です。仕様には次の記述があります。
There is always a
mainbranch reference pointing to the current-snapshot-id even if the refs map is null.
The
mainbranch never expires.
つまり main は特別扱いで、明示的に refs へ書かれていなくても存在し、期限切れにもなりません。
もうひとつ、branch はデータの分岐であってスキーマの分岐ではありません。仕様は次のように述べています。
the schema tracked for a table is valid across all branches
スキーマはテーブル全体で1つです。ブランチごとに異なるスキーマを持たせることはできません。
保持の観点では、branch や tag から参照されているスナップショットは expire されません。tag は「消えてほしくないスナップショットに錨を打つ」操作であり、同時に「そのスナップショットが参照するデータファイルを解放できなくする」操作でもあります。
処理の流れ
lab.versionedテーブルを作り直し、3回に分けて append します。各 append の間にtime.sleep(1)を挟み、コミットのタイムスタンプを分離します。各世代の snapshot ID を控えたうえで、
inspect.snapshots()で履歴を表示します。スナップショット ID を指定してスキャンします。
for label, sid in (("世代1", snap1), ("世代2", snap2), ("世代3", snap3)):
n = tbl.scan(snapshot_id=sid).to_arrow().num_rows- 世代2 に tag を打ち、
inspect.refs()で refs の中身を確認します。
with tbl.manage_snapshots() as ms:
ms.create_tag(snapshot_id=snap2, tag_name="release-2026-07")同じく世代2 を指す
auditブランチを作ります。Write-Audit-Publish は、この監査用ブランチに書いて検証してから main へ fast-forward する運用です。ただし fast-forward は Spark の procedure であり、PyIceberg には該当機能がありません。世代1 へロールバックし、
inspect.history()でis_current_ancestorを確認します。
tbl.manage_snapshots().rollback_to_snapshot(snap1).commit()実測結果
3世代のスナップショットは次のようになりました。
--- 3回に分けてデータを書き、3つのスナップショットを作ります
世代1: snapshot_id=7742509384658575965, 行数=1
世代2: snapshot_id=3695037040879739491, 行数=3
世代3: snapshot_id=1545959734487094455, 行数=4
履歴では parent_id が1つ前の snapshot ID を指しており、コミットが線形に連なっていることが読み取れます。
--- スナップショット履歴
committed_at snapshot_id parent_id operation
2026-07-18 13:49:32.679 7742509384658575965 NaN append
2026-07-18 13:49:33.789 3695037040879739491 7.742509e+18 append
2026-07-18 13:49:34.894 1545959734487094455 3.695037e+18 append
スナップショット ID を指定した読み取りは、それぞれの時点の行数を返します。
--- スナップショット ID でタイムトラベル
世代1 (id=7742509384658575965): 1 行
世代2 (id=3695037040879739491): 3 行
世代3 (id=1545959734487094455): 4 行
tag と branch を作ったあとの refs は次のとおりです。main は最新の世代3 を、audit ブランチと release-2026-07 タグは世代2 を指しています。
name type snapshot_id max_reference_age_in_ms min_snapshots_to_keep max_snapshot_age_in_ms
main BRANCH 1545959734487094455 NaN NaN NaN
audit BRANCH 3695037040879739491 NaN NaN NaN
release-2026-07 TAG 3695037040879739491 NaN NaN NaN
保持設定の列がすべて NaN であることに注意してください。参照ごとの保持ポリシー(max-reference-age-ms など)は指定していないため、これらの参照は既定で無期限に残ります。
ロールバックは行数を4行から1行に戻しました。
--- 世代1 にロールバックします
ロールバック前: 4 行
ロールバック後: 1 行
ロールバック後の履歴には、捨てられたコミットが is_current_ancestor=False として残っています。
made_current_at snapshot_id parent_id is_current_ancestor
2026-07-18 13:49:32.679 7742509384658575965 NaN True
2026-07-18 13:49:33.789 3695037040879739491 7.742509e+18 False
2026-07-18 13:49:34.894 1545959734487094455 3.695037e+18 False
2026-07-18 13:49:35.140 7742509384658575965 NaN True
最終行は「世代1 を再び current にした」というエントリです。ロールバックはスナップショットの削除ではなく、参照の付け替えとして記録されます。世代2・世代3 のスナップショットは消えていません。しかもこの実行では世代2 に tag と branch が付いているため、仮に expire を回しても世代2 は残ります。
ここから分かること
タイムトラベルが成立するのは、古いデータファイルがスナップショットから参照され続けているからです。裏返すと、expire_snapshots を実行しない限りストレージは解放されません。ロールバックしても容量は減りません。
tag の運用コストは見落とされやすい点です。tag を1つ打つと、そのスナップショットと、それが参照するデータファイル一式が保持対象になります。参照ごとの保持ポリシーを設定しなければ、これは無期限です。
マネージドサービスではさらに影響が広がる場合があります。Amazon S3 Tables の公式ドキュメントは、テーブルにユーザー定義の tag や branch が存在すると、そのテーブル全体で snapshot management が失敗すると述べています。tag や branch 側の retention 期間が短くても同様とされています。この場合、自動メンテナンスの停止は例外としては現れず、課金の増加としてしか観測できません。
WAP を PyIceberg だけで完結させることはできません。branch の作成と書き込みまではできますが、main への fast-forward は Spark procedure です。ブランチ運用を前提にするなら、Spark を含む構成を最初から想定しておく必要があります。PyIceberg 側の機能範囲については 17. PyIceberg の制約を実証する を参照してください。メタデータ上で snapshot と refs がどう格納されているかは 15. メタデータ三層構造を覗く で扱います。