レイクハウスのテーブル形式 — Apache Iceberg をメタデータで理解する
元に戻すのはファイルのコピーではなくポインタの移動だ — タイムトラベル・ロールバック・タグ・ブランチ
一言でいうと
Icebergのコミットは、古いスナップショットを消さずに新しいスナップショットを追加するので、過去を読むこと(タイムトラベル)は古いスナップショットのリストを読むことであり、元に戻すこと(ロールバック)はmainポインターを古いスナップショットに移すことです。タグは、時点に名前と保持期間を与え、ブランチはmainとは別に伸びていくリネージを作ります。
なぜ必要なのか: 午前3時のDELETE
条件を1つ書き忘れたDELETEが、ある地域の注文を丸ごと消しました。従来のレイクなら、バックアップからファイルを探してコピーし、その間に入ってきたデータと混ざらないよう、手で合わせなければなりません。数時間かかり、その間ダッシュボードは間違った数字を見せ続けます。
Icebergでは、事故を起こしたDELETEもコミットなので新しいスナップショットを作っただけで、事故直前のスナップショットとそのファイルはそのまま残っています。元に戻すために必要なのは、「mainがどのスナップショットを指すか」を変える、1回のmetadataコミットです。
どう動くのか: 過去の読み取りと巻き戻し
Sparkのタイムトラベルは2通りです。
SELECT count(*) FROM lake.demo.events VERSION AS OF 1234567890123456789; -- 스냅샷 ID·브랜치·태그 이름
SELECT count(*) FROM lake.demo.events TIMESTAMP AS OF '2026-04-01 09:00:00';
このコードブロックの韓国語コメントは、VERSION AS OFにはスナップショットID・ブランチ・タグの名前を指定できる、という意味です。
そのスナップショットのマニフェストリストを読むだけなので、コストは現在のテーブルを読むのと同じです。条件は1つです。そのスナップショットとファイルが、まだ存在していることです。
元に戻すには、プロシージャが複数あります。
| プロシージャ | 役割 |
|---|---|
| rollback_to_snapshot | mainを、現在のリネージにある古いスナップショットに戻す |
| rollback_to_timestamp | その時刻にcurrentだったスナップショットに戻す |
| set_current_snapshot | 祖先でなくてもよいスナップショット(またはref)を、currentにする |
| cherrypick_snapshot | 1つのスナップショットの変更を、現在の状態の上に新しいスナップショットとして移す(appendと動的上書きのみ) |
| fast_forward | 1つのブランチを、別のブランチの最新スナップショットまで早送りする |
ロールバックは、新しいスナップショットを作らず、事故のスナップショットも消しません。mainが通ってきた道(snapshot-log)に、「事故 → 事故の直前」が1行増えるだけです。事故のスナップショットをなくすのは、あとの期限切れ処理です。
タグとブランチ: 名前付きの参照
仕様のsnapshot referencesは、refsをmetadataに置きます。各refは、スナップショットIDと種類(tag・branch)を持ち、保持ポリシーとしてmax-ref-age-ms(ref自体の寿命)を、ブランチならmin-snapshots-to-keep・max-snapshot-age-msを持ちます。mainは期限切れにならないブランチです。
タグは、1つのスナップショットに付けたラベルです。19桁のIDを覚える必要がなくなり、もっと重要なことに、期限切れ処理はタグが指すスナップショットを消しません。Branchingドキュメントは、監査用に週単位・月単位のスナップショットにタグを付け、RETAIN 7 DAYSのように保持期間を与える例を挙げています。保持ポリシーに従い、期限切れ処理はまず、max-ref-age-msを過ぎたrefを外し、残ったrefが指すスナップショットは保持します。
ブランチは、mainとは別に伸びていくリネージです。新しいデータをmainではなくブランチに先にコミットし、品質検査に通ったら、mainをそのブランチの先頭まで早送りします(fast_forward)。読み取る人はmainだけを見るので、検査前のデータを1度も見ません。ドキュメントはこれを監査ブランチ(write-audit-publish、WAP)と呼び、write.wap.enabledとspark.wap.branchで、既存のジョブを書き換えずにブランチへ書き込ませる方法を示しています。fast_forwardは、mainがブランチの祖先であるときだけ実行できるので、その間にmainに別のコミットが入っていたら、拒否されます。
現場での姿
事故のあとで元に戻す場合です。オンコール担当者は、SELECT * FROM 표.history(プレースホルダーはテーブル名です)でmainが通ってきたスナップショットを見て、事故直前のスナップショットをVERSION AS OFで読んで数字を確認したあと、rollback_to_snapshotを呼びます。数秒で終わります。事故の原因を調べるために、事故のスナップショットは残しておきます。
期限切れ処理が先に動いた場合です。毎日動くメンテナンス処理が5日より古いスナップショットを消すなら、1週間前の状態に戻る必要があるとわかったときには、すでに手遅れです。戻る必要が生じそうな時点(月末の締め、デプロイ直前)には、あらかじめタグを付けておきます。
検証されていないデータがダッシュボードに出た場合です。ロードジョブがmainに直接書き込むと、品質検査が失敗しても、もう手遅れです。ブランチに書いて検査してから公開すれば、失敗したデータはブランチにだけ残ります。
実務で本当に大切なこと
- ロールバックは、ポインターの移動です。ファイルをコピーせず、事故のスナップショットも消しません。
- タイムトラベルは、スナップショットとファイルが残っている間だけできます。期限切れ処理が、その限界を決めます。
- 戻る時点には、期限切れの前にタグを付けます。タグの保持期間が、期限切れに勝ちます。
- 新しいデータはブランチで検査し、fast_forwardで公開します。
次のラボですること
3日分を1日1コミットずつ入れたあと、わざと1つの地域を消す事故を起こします。VERSION AS OFで事故直前の行数を読み、そのスナップショットに7日保持でタグを付け、rollback_to_snapshotでmainを元に戻します。そのあと、ブランチfixを作って翌日のデータをブランチにだけ書き込み、fast_forwardでmainを早送りして公開します。