レイクハウスとテーブルフォーマット
安価なオブジェクトストレージ上のParquetの山に、ウェアハウス並みのACID・タイムトラベル・スキーマ進化を後付けしたい人へ。Iceberg/Delta/Hudiがメタデータ層でこれをどう実現するかを内部構造から解説します。
- 1.レイクハウスはデータレイク(安価なオブジェクトストレージ上のParquet等)に、テーブルフォーマット(Iceberg/Delta/Hudi)というメタデータ層をかぶせ、どのファイル群が『いまのテーブル』かをスナップショットとして定義する。書き込みはファイルを追加し、メタデータのポインタを差し替えるだけで、既存ファイルは書き換えない(不変ファイル+可変メタデータ)。
- 2.オブジェクトストレージにはロックも複数オブジェクトのトランザクションも無いが、テーブルフォーマットは『メタデータの最新ポインタを指す1点をアトミックに切り替える』ことに一貫性を集約する。この切り替えを条件付き(compare-and-swap)にし、競合したライタは楽観的並行制御で再試行することでスナップショット隔離のACIDを得る。
- 3.各スナップショットは特定時点の全ファイル集合を指すため、過去スナップショットを読めばタイムトラベルになる。スキーマは列に不変のフィールドIDを振って管理し、列の追加・削除・改名を既存ファイル書き換え無しで安全に行える。パーティションもデータ値からの変換として宣言し、後から定義変更できる。
レイクとウェアハウスの断層をどう埋めるか
分析基盤には長く二つの世界がありました。データウェアハウスはテーブル・トランザクション・スキーマ・SQL最適化を備える一方、ストレージと計算が密結合で高価です。データレイクはS3やGCSのようなオブジェクトストレージに生ファイル(多くはParquetのような列指向形式)を安く大量に置けますが、そこにあるのは「ファイルの山」でしかありません。ディレクトリに part-0001.parquet が並ぶだけでは、どのファイルがいまのテーブルの中身なのか、書き込み途中のファイルが混ざっていないか、誰も保証してくれません。
レイクハウスは、この断層を「ファイルの山の上に薄いメタデータ層を敷く」ことで埋めます。ストレージは安いオブジェクトストレージのまま、その上にテーブルフォーマット(Apache Iceberg、Delta Lake、Apache Hudi)が「テーブルとは何か」を定義するメタデータを重ねる。データファイル自体はウェアハウスに取り込まず、レイクに置いたまま、ウェアハウス並みのACID・タイムトラベル・スキーマ進化を得ます。鍵は、データファイルを不変(immutable)に保ち、可変なのはメタデータだけという分離です。
テーブルフォーマットの中心概念はスナップショット(Deltaでは各コミットが作るバージョン)です。スナップショットは「この時点でテーブルを構成するデータファイルの完全な一覧」を指します。書き込みは既存ファイルを書き換えず、新しいデータファイルを追加し、それらを含む新しいスナップショットを作り、テーブルが指す最新スナップショットのポインタを差し替える。読み取りは常に一つのスナップショットに固定されるため、書き込み途中の中間状態が見えません。これがオブジェクトストレージ上でのスナップショット隔離の土台です。
オブジェクトストレージの制約と、一貫性の一点集中
なぜウェアハウスのように素直にいかないのか。原因はオブジェクトストレージの制約です。S3等はファイルシステムに見えて、実際はPUT/GET/LIST程度のキー・バリューストアです。ここには次がありません。
- 複数オブジェクトをまとめて更新するマルチオブジェクト・トランザクション。
- ディレクトリ単位のリネームのアトミック性(
LIST+個別コピーの模倣にすぎない)。 - 汎用のロック機構。
つまり「10個のParquetを追加し、3個を削除扱いにする」を一発でアトミックに行う手段が無い。テーブルフォーマットの設計思想は、この難しさを分散させず、『どのメタデータが最新か』を指すただ一点の切り替えにすべての一貫性を集約することです。データファイルの追加は、まだ誰からも参照されないので何個書こうと危険がない。危険なのは「テーブルの正体を切り替える瞬間」だけであり、そこだけをアトミックにすればよい。
この一点の切り替え方式が実装ごとの個性になります。
| フォーマット | 最新ポインタの切り替え方 | アトミック性の担保 |
|---|---|---|
| Iceberg | カタログが指すメタデータファイルのポインタを差し替える | カタログ(REST/Glue/Nessie等)の条件付き更新(CAS) |
| Delta Lake | トランザクションログ _delta_log に連番のJSONを1件追加する | オブジェクトストレージの条件付き作成(put-if-absent)や外部の相互排他 |
| Hudi | タイムライン上に完了マーカーを持つコミットを発行する | タイムラインのアトミックな完了操作 |
いずれも本質は**compare-and-swap(CAS)**です。「自分が読んだ時点の最新はNだった。Nのままなら N+1 へ進めてよい」を条件付きで行い、条件が崩れていれば失敗させる。ロックの代わりにこの条件付き更新一発で相互排他を実現するのが、レイクハウスのトランザクションの心臓部です。
楽観的並行制御でACIDを組み立てる
CASを土台にすると、並行書き込みは自然と**楽観的並行制御(OCC)**になります。ペシミスティックにロックを取りにいくのではなく、「まず自分の変更を用意し、コミット直前に競合が無いか賭ける」方式です。手順は概ね次のようになります。
1) 現在の最新スナップショット S を読む(ベースを固定)
2) 新しいデータファイルを書き出す(まだ誰も参照しない不変ファイル)
3) S を基点に、追加/削除を反映した新メタデータ S' を用意する
4) 「最新が依然 S なら S' に切り替える」を条件付き(CAS)で試みる
成功 → コミット確定(ここが唯一のアトミック点)
失敗 → 誰かが先にコミットした。競合を判定し (1) から再試行
(4) が失敗するのは、(1) から (4) の間に別のライタがコミットしたときです。ここで競合検出が効きます。二者が別々のファイルを追加しただけ(例:異なるパーティションへの追記)なら、相手のコミットの上に自分の変更を積み直して再試行すれば整合します。しかし同じデータを削除・更新し合うような論理的衝突なら、素直に積み直すと不整合になるため、フォーマットは検証(削除対象ファイルがまだ生きているか等)を行い、危険なら中断させます。これが分離性(Isolation)の実体で、多くの実装が既定でスナップショット隔離(各トランザクションは開始時点の一貫像を読む)を提供します。
OCCが安く成立するのは、データファイルが不変だからです。書き込み中のファイルは新しいキーに PUT され、既存ファイルは一切変更されない。だから「まだコミットしていない自分の書き込み」は他者から一切見えず、CASに負けても書いたファイルを捨てる(または次の試行で使い回す)だけで済み、途中まで書けた壊れた状態が表に出ることがありません。原子性(Atomicity)と分離性を、ロックではなく不変性+一点CASへ翻訳したのがレイクハウスの設計です。
メタデータの階層:ファイルを列挙せずに絞り込む
スナップショットが「全ファイル一覧」を指すといっても、ペタバイト級では一覧が数百万エントリに達します。毎回それをLISTしていては遅い。そこでテーブルフォーマットはメタデータを階層化します。Icebergを例にとると、概ね次の入れ子です。
カタログ → メタデータファイル(表スキーマ/スナップショット履歴)
→ マニフェストリスト(スナップショットを構成するマニフェスト群)
→ マニフェスト(データファイル群 + 各ファイルの統計)
→ データファイル(Parquet等の実データ)
各マニフェストは、含むデータファイルごとに列単位の統計(各列の最小値・最大値・null数・行数など)を保持します。クエリが WHERE event_date = '2026-06-21' のような述語を持つとき、エンジンはデータを開く前にマニフェストの統計を見て、その値域を含み得ないファイルを丸ごと読み飛ばせます。加えてマニフェストリストはマニフェストごとのパーティション値域も持つため、マニフェスト自体を開かずに丸ごとスキップできます。これがプルーニング(file/partition pruning)です。統計の突き合わせ自体はメタデータエントリ数に比例する走査ですが(データ値に対する木構造の索引ではない)、実データを開く前にメタデータだけで完結するため実データのLIST・スキャンを回避でき、実際に開くデータファイル数は述語とデータの並び(クラスタリング)次第で大きく減らせます。列指向ファイルそのものの内部圧縮やページ単位スキップ(列ファイル形式の詳細は /database/ 側の話題)とは層が違い、ここではファイル単位でどれを開くかをメタデータだけで決める点が要点です。
Deltaは階層は異なり、_delta_log にコミットごとの差分(add/removeアクション)を追記し、定期的にチェックポイント(それまでの全アクションを畳み込んだParquet)を作ります。読み取りは「直近チェックポイント+以降の差分」を再生して現在のファイル集合を求める。追記ログ+定期スナップショットという構造は、分散ログの畳み込み(/devops/ の領域)と同じ発想です。いずれの方式も、現在の状態=不変な履歴の畳み込みという点で一致しています。
タイムトラベル:過去スナップショットは消さない限り読める
スナップショットが「その時点の完全なファイル集合」を指すという性質は、そのままタイムトラベルを生みます。新しいコミットは古いスナップショットを上書きしない(メタデータ履歴に積み増すだけで、古いスナップショットが指すデータファイルもまだ削除されない)ため、過去のスナップショットIDやタイムスタンプ、あるいはコミットのバージョン番号を指定すれば、その時点のテーブルをそのまま読み直せます。監査、誤更新からの復旧、時点を固定した再現可能なML学習データの切り出しなどに直結します。
-- 概念例(構文はエンジン依存)
SELECT * FROM sales VERSION AS OF 42; -- スナップショット/バージョン指定
SELECT * FROM sales TIMESTAMP AS OF '2026-06-20'; -- 時刻指定
過去を読めるのは古いデータファイルを消していないからであり、放置すればストレージは単調に膨らみます。そこで各フォーマットは不要になったファイルを削除する保守処理を持ちます(Iceberg の expire_snapshots、Delta の VACUUM、Hudi の cleaner)。ここに落とし穴があります。保持期間より古いスナップショットを削除すると、そのバージョンへのタイムトラベルは即座に不可能になります。さらに危険なのは、まだ実行中の長時間クエリが参照しているファイルを消してしまうケースで、読み取り中のファイルが消えれば失敗します。保持期間は「最長クエリ時間+タイムトラベル要件」を満たすよう設定し、GCと参照の競合を避けるのが鉄則です。
スキーマ進化とパーティション進化:IDと変換で守る
長期運用では列の追加・削除・改名が必ず来ます。ここで素朴な実装は破綻します。列を名前や位置(何番目の列か)で対応づけると、列を消して別の列を足したときに、古いParquetの3番目の列を新しい別列と誤読しかねないからです。IcebergはこれをフィールドIDで解きます。テーブルの各列には作成時に不変の整数IDが振られ、読み取りは名前や位置ではなくIDでParquet内の列と突き合わせます。だから、
- 列の追加:新IDを割り当てるだけ。古いファイルにその列は無いので読むと
nullになる(後方互換)。 - 列の削除:IDを引退させるだけ。既存ファイルは書き換え不要。
- 改名:IDはそのまま名前だけ変える。データファイルには一切触れない。
いずれも既存データファイルを一つも書き換えずに完了します。これが「オブジェクトストレージ上でスキーマを安全に進化させる」の実体です。
さらにIcebergはパーティション進化を持ちます。パーティションをデータ値からの変換(例:日付列を月に丸める、ID列を N 個のバケットにハッシュする)として宣言し、しかもこの定義を後から変更できます。旧データは旧パーティション定義のまま、新データは新定義で書かれ、メタデータが両者を統一的に扱う。加えて隠しパーティショニングにより、利用者はパーティション列を意識せず素の述語(WHERE ts >= ...)を書くだけで、エンジンが変換を通じて対応パーティションへプルーニングします。これは、フォルダ名にパーティション値を直書きし利用者に意識させてきた旧来のレイクの弱点を、メタデータ側へ隠蔽したものです。
- 「なぜオブジェクトストレージ上でACIDが成立するのか」:複数オブジェクトを同時更新するのではなく、最新メタデータを指す一点をCASで切り替えるから。原子性はその一点に、分離はスナップショット隔離に集約される。
- 「Delta と Iceberg のメタデータの持ち方の違い」:Deltaは
_delta_logへの追記型トランザクションログ+チェックポイント、Icebergはメタデータ→マニフェストリスト→マニフェストの階層で、カタログが最新を指す。 - 「スキーマ進化が安全な理由」:列を位置や名前でなく不変フィールドIDで対応づけるため、追加・削除・改名で既存ファイルを書き換えずに済む。
- 「タイムトラベルの代償」:古いファイルを保持する必要があり、GC(expire/VACUUM)の保持期間がタイムトラベル可能範囲と実行中クエリの安全性を決める。
まとめ
- レイクハウスは安価なオブジェクトストレージ上のファイル群に、テーブルフォーマット(Iceberg/Delta/Hudi)というメタデータ層を重ね、ウェアハウス並みのACID・タイムトラベル・スキーマ進化を得る。データファイルは不変、可変なのはメタデータだけが根本原理。
- テーブルの実体はスナップショット=ある時点の全ファイル集合。書き込みは新ファイルを追加し新スナップショットへ最新ポインタを切り替えるだけで、既存ファイルは書き換えない。
- オブジェクトストレージにはマルチオブジェクト・トランザクションもロックも無いため、一貫性を『最新メタデータを指す一点』へ集約し、その切り替えをCAS(compare-and-swap)で行う。競合ライタは楽観的並行制御で再試行し、スナップショット隔離を得る。
- メタデータは階層化(Iceberg)や追記ログ+チェックポイント(Delta)で持ち、列統計によるプルーニングでファイルを
LISTせず候補を絞る。 - 過去スナップショットを保持する限りタイムトラベルが可能。ただしGC(expire/VACUUM)の保持期間がタイムトラベル範囲と実行中クエリの安全性を左右する。
- スキーマは不変フィールドIDで対応づけ、列の追加・削除・改名を既存ファイル書き換え無しで行う。パーティションも値からの変換として宣言し、隠しパーティショニングと後からのパーティション進化を可能にする。
データ工学 Article
レイクハウスとテーブルフォーマットを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
レイクハウス
比較で見る軸
難易度: advanced / カテゴリ: データ工学 / タグ数: 6
導入後に効く点
オブジェクトストレージにはロックも複数オブジェクトのトランザクションも無いが、テーブルフォーマットは『メタデータの最新ポインタを指す1点をアトミックに切り替える』ことに一貫性を集約する。この切り替えを条件付き(compare-and-swap)にし、競合したライタは楽観的並行制御で再試行することでスナップショット隔離のACIDを得る。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データ工学
- タグ数
- 6
判断チェックリスト
- 自社の用途が「レイクハウス / Iceberg」に近いか確認する。
- 強みである「レイクハウスはデータレイク(安価なオブジェクトストレージ上のParquet等)に、テーブルフォーマット(Iceberg/Delta/Hudi)というメタデータ層をかぶせ、どのファイル群が『いまのテーブル』かをスナップショットとして定義する。書き込みはファイルを追加し、メタデータのポインタを差し替えるだけで、既存ファイルは書き換えない(不変ファイル+可変メタデータ)。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。