以下の内容はhttps://techblog.forgevision.com/entry/iceberg-mv-create-glue-jobより取得しました。


Apache Iceberg Materialized Viewを試してみた① - Glue Job でMVを作る -

こんにちは、開発グループの寺田です。 この記事では AWS Glue の Apache Iceberg Materialized View を実際に動かした検証をまとめています。

はじめに

外部システムからデータハブ内のデータを安全に参照させたい、という要件はデータ基盤を運用していると必ずといっていいほど出てきます。

そこで注目したのが AWS Glue に追加された Apache Iceberg の Materialized View(以下 MV) です。MV を「外部向けの安全なデータ参照レイヤー」として活用できるのではないかと考え、検証を始めました。

この記事はその検証シリーズの第1弾です。まずは MV を実際に作成するところまでを、手順とポイントをまとめながら紹介します。

シリーズ構成
- ① Glue Job で MV を作る ← 本記事
- ② 外部システムからデータを参照する (掲載予定)
- ③ Lake Formation で参照範囲を制御する (掲載予定)


ユースケースのイメージ

今回の検証のベースにあるユースケースは以下のようなイメージです。

[ 内部データ(rawテーブル)]
         ↓  Glue Job(Iceberg 形式で書き込み)
[ Iceberg テーブル群 ]
         ↓  Glue Job(MV 作成)
[ Materialized View ]  ← 外部システムはここだけ参照
         ↓
[ 外部システム / Athena ]

内部のrawテーブルに直接アクセスさせるのではなく、MV を参照専用の公開レイヤーとして置く構成です。MV は集計済みデータを保持しているため、クエリのたびに重い JOIN・集計処理が走らず、外部システムからの参照コストも抑えられます。


検証環境

コンポーネント 内容
ETL AWS Glue Job(version 4.0)Iceberg 形式で書き込み
テーブル形式 Apache Iceberg v2
メタデータ管理 AWS Glue Data Catalog
MV 作成 AWS Glue Job(version 5.1
ストレージ Amazon S3 汎用バケット(※S3 Tablesとの比較は後述)
クエリエンジン Amazon Athena v3

📝 S3 汎用バケット vs S3 Tables
Iceberg テーブルの保存先は S3 汎用バケットと S3 Tables バケットの2択があります。今回は PoC のデータ規模(50万件)では性能差がほぼ出ないことから汎用バケットを選択しました。S3 Tables は Compaction・スナップショット管理が自動でフルマネージドですが、S3 Standard と比較してコストが約37%高いです。本番運用でデータ量・クエリ頻度が高い場合は S3 Tables の採用を検討してください。

項目 S3 Tables S3 汎用バケット
クエリ性能 最大3倍高速(AWS公称) 基準
トランザクション 最大10倍高い TPS 基準
メンテナンス Compaction・スナップショット管理が自動 手動または別途 Glue Job が必要
コスト S3 Standardより約37%高い S3 Standard料金

前提条件

Glue バージョンは 5.1 以上が必須

Iceberg の Materialized View は Glue version 5.1 以上でのみサポートされています。4.0 以下では CREATE MATERIALIZED VIEW 構文が動作しないため、必ず 5.1 を指定してください。

ソーステーブルは Iceberg 形式で用意する

MV のソースとなるテーブルは Iceberg フォーマットで Glue Data Catalog に登録されている必要があります。今回は以下の 4 テーブルを事前に Iceberg 形式で用意しています。

テーブル名 内容 レコード数
customers 顧客マスタ 1,000 件
products 商品マスタ 500 件
orders 注文ヘッダ 100,000 件
order_items 注文明細 500,000 件

Glue Job の設定

MV 作成ジョブの --conf パラメータには以下を設定します。

spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
spark.sql.catalog.glue_catalog=org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.glue_catalog.type=glue
spark.sql.catalog.glue_catalog.warehouse=s3://<YOUR_BUCKET>/
spark.sql.catalog.glue_catalog.glue.region=<YOUR_REGION>
spark.sql.catalog.glue_catalog.glue.id=<YOUR_ACCOUNT_ID>
spark.sql.catalog.glue_catalog.glue.account-id=<YOUR_ACCOUNT_ID>
spark.sql.defaultCatalog=glue_catalog
spark.sql.optimizer.answerQueriesWithMVs.enabled=true
spark.sql.materializedViews.metadataCache.enabled=true

Glue Job が正常に完了すると、以下のように Succeeded ステータスが確認できます。

Glue Job 実行成功


MV 作成スクリプト

CREATE MATERIALIZED VIEW の構文

今回作成する MV は、4 テーブルを JOIN して顧客×地域×カテゴリ別の日次売上を集計するビューです。

CREATE MATERIALIZED VIEW glue_catalog.{database}.{mv_name}
AS
SELECT
  c.customer_id,
  c.customer_name,
  c.region,
  p.category,
  o.order_date,
  COUNT(o.order_id)                    AS order_count,
  SUM(oi.quantity)                     AS total_quantity,
  SUM(oi.quantity * oi.unit_price)     AS total_sales
FROM
  glue_catalog.{database}.customers   c
  JOIN glue_catalog.{database}.orders      o  ON c.customer_id = o.customer_id
  JOIN glue_catalog.{database}.order_items oi ON o.order_id    = oi.order_id
  JOIN glue_catalog.{database}.products    p  ON oi.product_id = p.product_id
GROUP BY
  c.customer_id, c.customer_name, c.region, p.category, o.order_date

Glue Job スクリプト(全体)

import sys
from awsglue.utils import getResolvedOptions
from awsglue.context import GlueContext
from awsglue.job import Job
from pyspark.context import SparkContext

args = getResolvedOptions(sys.argv, [
    'JOB_NAME',
    'CATALOG_DATABASE',
    'S3_WAREHOUSE_PATH',
    'MV_NAME'
])

sc          = SparkContext()
glueContext = GlueContext(sc)
spark       = glueContext.spark_session
job         = Job(glueContext)
job.init(args['JOB_NAME'], args)

catalog_database = args['CATALOG_DATABASE']
mv_name          = args['MV_NAME']

# defaultCatalog を設定
spark.conf.set("spark.sql.defaultCatalog", "glue_catalog")
spark.conf.set("spark.sql.optimizer.answerQueriesWithMVs.enabled", "true")

# 冪等性確保:既存 MV を削除してから再作成
spark.sql(f"DROP MATERIALIZED VIEW IF EXISTS glue_catalog.{catalog_database}.{mv_name}")

# MV 作成
create_mv_sql = f"""
CREATE MATERIALIZED VIEW glue_catalog.{catalog_database}.{mv_name}
AS
SELECT
  c.customer_id,
  c.customer_name,
  c.region,
  p.category,
  o.order_date,
  COUNT(o.order_id)                    AS order_count,
  SUM(oi.quantity)                     AS total_quantity,
  SUM(oi.quantity * oi.unit_price)     AS total_sales
FROM
  glue_catalog.{catalog_database}.customers   c
  JOIN glue_catalog.{catalog_database}.orders      o  ON c.customer_id = o.customer_id
  JOIN glue_catalog.{catalog_database}.order_items oi ON o.order_id    = oi.order_id
  JOIN glue_catalog.{catalog_database}.products    p  ON oi.product_id = p.product_id
GROUP BY
  c.customer_id, c.customer_name, c.region, p.category, o.order_date
"""

spark.sql(create_mv_sql)
job.commit()

実装上の制約

MV 定義のクエリには制約があります。特に増分更新(インクリメンタルリフレッシュ)を活かすには書き方に注意が必要です。詳細は公式ドキュメントの「Considerations and limitations」を参照してください。


実装のポイントまとめ

FROM 句は 3 階層フルパスで書く

defaultCatalog を設定していても、MV クエリ内のテーブル参照は glue_catalog.{database}.{table} のフルパスで記述します。省略するとテーブル解決エラーが発生することがあります。

DROP IF EXISTS で冪等性を確保する

MV の再作成や定義変更の際、既存 MV が残っていると CREATE が失敗します。スクリプト冒頭で DROP MATERIALIZED VIEW IF EXISTS を実行することで、何度実行しても安全に再作成できます。

⚠️ 商用運用での注意
DROP から CREATE が完了するまでの間、MV が存在しない状態になるため外部システムからのクエリがエラーになる瞬断リスクがあります。 また、Iceberg のスナップショット履歴が消えること、増分更新の蓄積が初期化されフルリフレッシュが毎回走ることにも注意が必要です。
なお、ALTER MATERIALIZED VIEW は更新スケジュールの変更のみに対応しており、MV の定義(クエリ内容)の変更には使えません。
CREATE OR REPLACE MATERIALIZED VIEW も非サポートのため、定義変更は現時点では DROP → CREATE 以外の方法はありません。
Managing materialized views 参照)
商用運用では定期的なデータ更新には REFRESH MATERIALIZED VIEW のみを実行し、 定義変更が必要な場合はメンテナンス時間を設けるか、新しい MV を別名で作成して参照先を切り替える方法を推奨します。


作成した MV の確認

Glue Job が正常に完了すると、Glue Data Catalog に MV が登録され、S3 に Iceberg 形式でデータが保存されます。

項目
MV 名 sales_summary_mv
データベース poc_iceberg
フォーマット Apache Iceberg v2(Parquet)
集計グラニュラリティ 顧客 × 地域 × カテゴリ × 日次

Glue Data Catalog 上で MV が登録されていることを確認できます。

Athena から以下のクエリで動作確認できます。

SELECT
  region,
  category,
  SUM(order_count)  AS total_orders,
  SUM(total_sales)  AS total_revenue
FROM   poc_iceberg.sales_summary_mv
GROUP BY region, category
ORDER BY total_revenue DESC;

補足: Athena からクエリするユーザー/ロールには、Lake Formation で当該 MV テーブルへの SELECT 権限を付与する必要があります。


リフレッシュの動作確認

MV はソーステーブルのデータが更新された際にリフレッシュすることで、最新の状態を保持できます。ここでは新しい注文データを追加し、リフレッシュ後に差分が反映されることをビフォーアフター形式で確認します。

⚠️ 注意: REFRESH MATERIALIZED VIEW は Athena ではサポートされていません。リフレッシュは必ず Glue Job(Spark)経由で実行してください。

今回の MV はフルリフレッシュで動作する

今回作成した sales_summary_mvCOUNT + SUM の集計関数を使用しています。 公式ドキュメントの制限事項に「増分更新では集計関数を含めることはできない」と明記されているため、フルリフレッシュにフォールバックして動作します。 制限事項の詳細は公式ドキュメントの「Considerations and limitations」を参照してください。

手順

1. リフレッシュ前の MV を Athena でクエリする

SELECT
  region,
  category,
  order_date,
  SUM(order_count)    AS order_count,
  SUM(total_quantity) AS total_quantity,
  SUM(total_sales)    AS total_sales
FROM poc_iceberg.sales_summary_mv
WHERE region    = '東京'
  AND order_date = DATE '2026-03-23'
GROUP BY region, category, order_date;

2. ソーステーブルに新しいレコードを追加する

orders・order_items テーブルに注文日が2026-03-23のデータを追加登録します。

3. Glue Job で MV をリフレッシュする

リフレッシュ専用の Glue Job を実行します。MV 作成ジョブとは別ファイルとして分離しています。

# glue_job_refresh_mv.py
# Glue version 5.1 以上が必要

import sys
from awsglue.utils import getResolvedOptions
from awsglue.context import GlueContext
from awsglue.job import Job
from pyspark.context import SparkContext

args = getResolvedOptions(sys.argv, [
    'JOB_NAME',
    'CATALOG_DATABASE',
    'MV_NAME'
])

sc          = SparkContext()
glueContext = GlueContext(sc)
spark       = glueContext.spark_session
job         = Job(glueContext)
job.init(args['JOB_NAME'], args)

catalog_database = args['CATALOG_DATABASE']
mv_name          = args['MV_NAME']

# defaultCatalog を設定
spark.conf.set("spark.sql.defaultCatalog", "glue_catalog")
spark.conf.set("spark.sql.optimizer.answerQueriesWithMVs.enabled", "true")

# MV リフレッシュ
spark.sql(f"REFRESH MATERIALIZED VIEW glue_catalog.{catalog_database}.{mv_name}")

job.commit()

4. リフレッシュ後の MV をクエリして差分が反映されていることを確認する

SELECT
  region,
  category,
  order_date,
  SUM(order_count)    AS order_count,
  SUM(total_quantity) AS total_quantity,
  SUM(total_sales)    AS total_sales
FROM poc_iceberg.sales_summary_mv
WHERE region    = '東京'
  AND order_date = DATE '2026-03-23'
GROUP BY region, category, order_date;

リフレッシュタイプの確認

リフレッシュタイプは CloudWatch Logs または Glue のログで確認できます。

フルリフレッシュの場合、Glue のログに以下のような出力が確認できます。

lastRefreshType=FULL

増分更新の場合、CloudWatch Logs に以下の DEBUG ログが出力されます。

DEBUG RefreshMaterializedViewExec: Executed Incremental Refresh

補足: 自動更新を設定する場合は ALTER MATERIALIZED VIEW でスケジュールを指定できます。

ALTER MATERIALIZED VIEW poc_iceberg.sales_summary_mv ADD SCHEDULE REFRESH EVERY 2 HOURS;

今回の sales_summary_mv は集計関数(COUNT + SUM)を含むため、ログに lastRefreshType=FULL が出力されフルリフレッシュで動作していることが確認できました。

補足: 自動更新を設定する場合は Glue Job から ALTER MATERIALIZED VIEW でスケジュールを指定できます。 python spark.sql(""" ALTER MATERIALIZED VIEW glue_catalog.{catalog_database}.{mv_name} ADD SCHEDULE REFRESH EVERY 2 HOURS """) 今回の検証では手動更新での動作確認にとどめています。


まとめ

Glue 5.1 × Iceberg の組み合わせで Materialized View の作成ができました。

作成にあたって押さえておくべきポイントを整理します。

# ポイント
1 Glue version は 5.1 以上が必須
2 --conftype=glueglue.region + MV 最適化フラグを設定する
3 FROM 句はフルパス(3階層)で記述する
4 DROP IF EXISTS で冪等性を確保する(PoC 環境向け)
5 REFRESH MATERIALIZED VIEW は Athena 非サポート。Glue Job(Spark)経由で実行する
6 集計関数・ウィンドウ関数を含む MV はフルリフレッシュで動作する(増分更新非対応)

MV 自体は JOIN・集計の結果を事前計算して保持する仕組みです。外部システムが毎回重いクエリを実行する代わりに、MV を参照するだけでよくなるため、外部向けの安全・軽量なデータ参照レイヤーとして活用できる可能性を感じています。

次回は実際に外部システムから MV を参照する構成を検証します。


参考リンク





以上の内容はhttps://techblog.forgevision.com/entry/iceberg-mv-create-glue-jobより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14