本記事はIceberg Advent Calendar 2025 14日目の記事です。
Apache Icebergコミュニティでは、Materialized View(マテリアライズドビュー、以下MV)の仕様策定が進んでいます。PR #11041はマージに向けた最終調整段階にあり、複数のコミッターからApproveを受けています。本記事では、最新の検討状況をご紹介します。
なお、本記事でご紹介する内容はマージ前の内容を基にしています。執筆時点での状況を見るに、現状の内容から大きく変わることはなさそうではあるものの、今後変わる可能性がある未確定の内容である点はご留意ください。
背景
MVは、クエリ定義を論理テーブルとして保存し、事前計算された結果を返すデータベースの機能です。クエリ実行コストを事前計算ステップに移すことで、頻繁に実行されるクエリの高速化に利用されます。
Icebergコミュニティでは2022年頃からIssue #6420でMVの議論が始まり、2024年3月にProposal #10043として正式な提案がまとめられました。
以下が具体的な仕様定義に向けたPRです。
github.com
まず抑えておきたい点として、Icebergテーブル仕様と同様に、MVも「仕様」です。つまり、仕様策定の中で定義されるのはMVの振る舞いの定義であり、具体的にはspec.mdとview-spec.mdが更新される予定です。(検討中のSpecの内容はリンクの通りです)
仕様が確定した後に、各種エンジンが仕様に基づくMVの参照や、更新に必要な実装をすることが期待されます。
設計の基本方針
Iceberg MVは既存のIceberg ViewとIceberg Tableを組み合わせて実現されます。
Icebergのメタデータ構造
まず、通常のIcebergテーブルのメタデータ構造を振り返ります。Icebergテーブルは、メタデータファイル、マニフェストリスト、マニフェストファイル、データファイルという階層構造で管理されています。メタデータファイルにはスキーマ、パーティション仕様、スナップショットの履歴が記録され、各スナップショットはマニフェストリストを通じてデータファイルを参照します。

Iceberg Viewも同様にメタデータファイルを持ちますが、データファイルは持ちません。代わりに、クエリ定義(SQL)とスキーマ、バージョン履歴を保持します。Viewはデータを格納せず、クエリ実行時に定義されたSQLが展開されます。
通常のViewの参照構造
通常のIceberg Viewは、クエリ定義を通じてソーステーブルを参照します。Viewに対してクエリが実行されると、クエリエンジンはViewのSQL定義を展開し、ソーステーブルのデータを読み取って結果を計算します。

この構造では、Viewへのクエリのたびにソーステーブルからデータを読み取り、集計などの計算を実行します。ソーステーブルが大きい場合や、複雑な集計を行う場合、クエリのたびに計算コストが発生します。
ViewとTableを組み合わせによるMVの実現
MVは、このViewとTableを組み合わせることで実現されます。Iceberg Viewがクエリ定義やMV固有のメタデータを保持し、Storage Tableと呼ばれる通常のIceberg Tableが事前計算されたデータを格納します。ViewからStorage Tableへのポインタを持つことで、両者が関連付けられます。

Storage Tableは通常のIcebergテーブルと同じ構造を持つため、Icebergの機能をそのまま活用できます。タイムトラベル、スキーマ進化、パーティショニング、各種ファイルフォーマット(Parquet、ORC、Avro)のサポートなど、Icebergテーブルの機能はすべてStorage Tableでも利用可能です。
この設計により、既存のView SpecとTable Specを拡張する形でMVを実現できます。新しいファイルフォーマットやメタデータ構造を導入する必要がないため、既存のIcebergエコシステム(カタログ、クエリエンジン、ツール)との互換性を維持しやすくなっています。
View Metadataの拡張
MVを実現するために、View Specには2つの拡張が加えられます。
1つ目は、View Versionへのstorage-tableフィールドの追加です。このフィールドにはStorage Tableの識別子(namespaceとname)が格納されます。storage-tableが設定されていればMV、設定されていなければ通常のViewとして扱われます。
2つ目は、View Metadataへのmax-staleness-msフィールドの追加です。このフィールドは、ソーステーブルに変更があった後、どの程度の時間までStorage Tableのデータをfresh(新鮮)とみなすかを定義します。通常のViewではこのフィールドをnullに設定する必要があります。MVでnullの場合、Storage Tableのデータは常にfreshとみなされます。
Storage Tableとrefresh-state
Storage Tableは通常のIceberg Tableですが、MVの鮮度判定に必要な情報を追加で保持します。この情報はrefresh-stateと呼ばれ、Snapshot SummaryにJSON文字列として格納されます。
refresh-stateは、MVのリフレッシュ実行時点でのソーステーブル・ビューの状態を記録します。記録される情報は、リフレッシュ実行時のMVのversion-id、ソーステーブルの状態リスト、ソースビューの状態リスト、そしてリフレッシュ開始時刻です。
ソーステーブルの状態としては、テーブルのUUID、リフレッシュ時のsnapshot-id、参照しているブランチ名が記録されます。ソースビューの状態としては、ビューのUUID、リフレッシュ時のversion-idが記録されます。
ソースの追跡範囲
refresh-stateにどの範囲のソースを記録するかは、仕様策定の過程で議論されたポイントの1つです。
議論の結果、直接参照と間接参照の両方が記録されますが、MVを経由した間接参照は除外されることになりました。例えば、あるMVがView1を参照し、View1がTable1とTable2を参照している場合、source-table-statesにはTable1とTable2の両方が記録されます。一方、MVが別のMV2を参照している場合、MV2のStorage Tableの状態は記録されますが、MV2が参照するテーブルの状態は記録されません。

上図では、Table1とTable2はView1経由の間接参照として記録されますが、Table3はMV2経由の間接参照のため記録されません。MV2自体とそのStorage Tableは記録されます。
この設計の意図は、MVを経由した間接参照の鮮度判定をクエリエンジンに委ねることにあります。クエリエンジンが再帰的な鮮度評価を行う場合は、クエリツリーを展開して判定します。
鮮度状態の解釈
読み取り時、クエリエンジンはMVの状態をinvalid、fresh、staleの3つに分類して解釈します。

invalidは、MVの現在のversion-idがrefresh-stateに記録されたview-version-idと一致しない状態を指します。これは、MVの定義が変更されたがリフレッシュがまだ実行されていない場合に発生します。invalidの場合、Storage Tableのデータを使用した読み取りは実行できません。
freshは、validかつStorage Tableのデータが定義されたStaleness Window内のある時点でクエリを実行した結果と一致する状態を指します。staleは、validだがfreshではない状態です。
これらの状態はメタデータに直接記録されるわけではなく、クエリエンジンがrefresh-stateとソーステーブルの現在の状態を比較して判定します。
MVの動作の流れ
MVの操作がどのような流れになるか、具体ていを基に考えてみましょう。(以下の例ではSQLを用いてMVを操作していますが、これらの構文は各エンジンの今後の実装次第であるため、ある種の疑似コードである点に留意してください)
salesテーブルには、個々の売上トランザクションが記録されています。
CREATE TABLE prod.analytics.sales ( transaction_id BIGINT, product_id BIGINT, amount DECIMAL(10, 2), sold_at TIMESTAMP ) USING iceberg;
日次の売上集計を事前計算するMVを作成します。
CREATE MATERIALIZED VIEW prod.analytics.daily_sales_summary AS SELECT DATE(sold_at) AS sale_date, COUNT(*) AS transaction_count, SUM(amount) AS total_amount FROM prod.analytics.sales GROUP BY DATE(sold_at);
このMVが作成されると、以下のようなメタデータが生成されます。
View Metadata(daily_sales_summary):
{ "view-uuid": "a1b2c3d4-...", "current-version-id": 1, "max-staleness-ms": 3600000, "versions": [{ "version-id": 1, "schema-id": 1, "default-namespace": ["analytics"], "representations": [{ "type": "sql", "sql": "SELECT DATE(sold_at) AS sale_date, COUNT(*) AS transaction_count, SUM(amount) AS total_amount FROM prod.analytics.sales GROUP BY DATE(sold_at)", "dialect": "spark" }], "storage-table": { "namespace": ["analytics"], "name": "daily_sales_summary__storage" } }] }
Storage Table Snapshot Summary(daily_sales_summary__storage):
{ "refresh-state": { "view-version-id": 1, "source-table-states": [{ "uuid": "sales-table-uuid-...", "snapshot-id": 1234567890 }], "source-view-states": [], "refresh-start-timestamp-ms": 1702700400000 } }
MVに対してクエリが実行されると、クエリエンジンは以下の判定を行います。
daily_sales_summaryのversion-id(1)と、Storage Tableのrefresh-stateに記録されたview-version-id(1)を比較 → 一致するのでvalidsalesテーブルの現在のsnapshot-idと、refresh-stateに記録されたsnapshot-id(1234567890)を比較- snapshot-idが一致すればfresh、異なる場合はmax-staleness-ms(1時間)以内かどうかで判定
freshと判定されれば、Storage Tableの事前計算データが返されます。staleの場合、エンジンの実装によってはStorage Tableを使用するか、元のクエリを実行するかを選択します。
MVを誰がいつ更新するか
MV仕様の興味深い(Icebergらしい)点として、MVをいつ誰がどのように更新するかについては、仕様として定義されていません。ただ、上記の通りMVのStale状態を判定するための仕組みは用意されており、任意の実装がStorage Tableを更新できます。
従って、MVの更新は個々のソフトウェア、サービスの実装に委ねられています。MVの仕様確定後、定期的にMVの状態をチェックして、更新を自動化するような実装がOSSやサービスプロバイダーから提供されていくものと思われます。
まとめ
IcebergにおけるMVは、既存のView SpecとTable Specを拡張する形で設計されています。MVはIceberg ViewとStorage Tableの組み合わせで実装され、View Metadataにstorage-tableとmax-staleness-msが追加されます。この設計により、異なるクエリエンジン間でMVを共有・管理できるオープンな仕様が実現されます。
仕様確定後、個々のエンジンやソフトウェアで使えるようになるまではまだ時間がかかるかと思いますが、今後が楽しみですね!