AWS Glue の Apache Iceberg ベースのマテリアライズドビューを試してみた #AWSreInvent

AWS Glue の Apache Iceberg ベースのマテリアライズドビューを試してみた #AWSreInvent

2025.12.27

クラウド事業本部の石川です。re:Invent2025期間中にAWS Glue の Apache Icebergベースのマテリアライズドビュー(Materialized View) のサポートが発表されました。マテリアライズドビューを使うことで、事前に計算した集計結果を保存しておき、クエリのパフォーマンスを向上させることができます。

https://dev.classmethod.jp/articles/aws-glue-apache-iceberg-awsreinvent/

今回は、AWS Glue ETLジョブを使用して、Icebergベースのマテリアライズドビューを実際に作成・リフレッシュする手順を試してみました。下記のブログを参考に、実際に動作するのに設定やスクリプトなどを変更しています。

https://aws.amazon.com/jp/blogs/news/introducing-apache-iceberg-materialized-views-in-aws-glue-data-catalog-2/

Apache Iceberg ベースのマテリアライズドビューとは

Apache Icebergベースのマテリアライズドビューは、以下の特徴を持っています。

  • SQLベースでMVを定義・作成できる
  • Amazon S3 Tables もしくは Iceberg形式でAmazon S3に保存される
  • 自動リフレッシュスケジュールを設定可能
  • フルリフレッシュとインクリメンタルリフレッシュをサポート
  • Amazon Athena、Amazon EMR、AWS Glue の Apache Spark エンジンは、自動クエリ書き換えが可能

アーキテクチャ概要

ベーステーブルとなるordersテーブルを作成して、初期データを投入します。ordersテーブルからrders_summary_mvというマテリアライズドビューを作成します。ordersテーブルにデータを追加した後、rders_summary_mvをリフレッシュして差分更新します。

前提条件

マテリアライズドビューを作成するには、以下の条件が必要です。

  • AWS Glue バージョン: 5.1 以上
  • ソーステーブル形式: Apache Iceberg のみ(Hive、Hudi、Delta Lakeは非サポート)
  • 命名規則: 3部名称(glue_catalog.database.table)必須
  • リージョン・アカウント: 同一リージョン、同一アカウント内

マテリアライズドビューを試す!

環境情報

項目
リージョン ap-northeast-1
Glue バージョン 5.1
Worker Type G.1X
Number of Workers 2

Step 1: S3バケットの準備

Icebergテーブルのデータを保存するS3バケットを作成します。

# データレイク用バケット作成
aws s3 mb s3://cm-lakehouse-20251226 --region ap-northeast-1

# warehouseディレクトリ作成
aws s3api put-object \
  --bucket cm-lakehouse-20251226 \
  --key warehouse/

Step 2: IAMロールの準備

Glue ETLジョブ用のIAMロールには、以下のポリシーをアタッチします。

  • AWSGlueServiceRole
  • AmazonS3FullAccess(本番環境では最小権限に絞ることを推奨)
  • Lake Formation用カスタムポリシー(lakeformation:GetDataAccess等)
# アタッチされたポリシーの確認
aws iam list-attached-role-policies \
  --role-name AWSGlueServiceRoleDefault

Step 3: Lake Formationの設定

GlueロールをLake Formation Data Lake管理者に追加し、S3データロケーションを登録します。

# データロケーションの登録
aws lakeformation register-resource \
  --resource-arn arn:aws:s3:::cm-lakehouse-20251226 \
  --use-service-linked-role \
  --region ap-northeast-1

# データロケーションへのアクセス権限付与
aws lakeformation grant-permissions \
  --principal '{"DataLakePrincipalIdentifier":"arn:aws:iam::123456789012:role/AWSGlueServiceRoleDefault"}' \
  --resource '{"DataLocation":{"ResourceArn":"arn:aws:s3:::cm-lakehouse-20251226"}}' \
  --permissions "DATA_LOCATION_ACCESS" \
  --region ap-northeast-1

Step 4: Glue ETLジョブの作成

ジョブパラメータ設定

20251226-aws-glue-iceberg-mv-1

色々と試行錯誤したのですが、Jobパラメーターに設定したのは以下の1つです。

パラメータ
--datalake-formats iceberg

20251226-aws-glue-iceberg-mv-2

SparkSession

色々と試行錯誤したのですが、新しいSparkSessionを作成せず、GlueContextのspark_sessionを使用することです。それ以外は、Jobパラメーターで設定しています。

from awsglue.context import GlueContext
from pyspark.context import SparkContext

# 設定パラメータ
ACCOUNT_ID = "123456789012"
REGION = "ap-northeast-1"
WAREHOUSE_PATH = "s3://cm-lakehouse-20251226/warehouse"

# GlueContext初期化
sc = SparkContext()
glueContext = GlueContext(sc)

# GlueContextのspark_sessionを使用
spark = glueContext.spark_session

# Spark設定を追加
spark.conf.set("spark.sql.catalog.glue_catalog", "org.apache.iceberg.spark.SparkCatalog")
spark.conf.set("spark.sql.catalog.glue_catalog.warehouse", WAREHOUSE_PATH)
spark.conf.set("spark.sql.catalog.glue_catalog.glue.region", REGION)
spark.conf.set("spark.sql.catalog.glue_catalog.client.region", REGION)
spark.conf.set("spark.sql.catalog.glue_catalog.glue.id", ACCOUNT_ID)
spark.conf.set("spark.sql.catalog.glue_catalog.glue.account-id", ACCOUNT_ID)
spark.conf.set("spark.sql.catalog.glue_catalog.type", "glue")
spark.conf.set("spark.sql.catalog.glue_catalog.glue.lakeformation-enabled", "false")
spark.conf.set("spark.sql.defaultCatalog", "glue_catalog")
spark.conf.set("spark.sql.optimizer.answerQueriesWithMVs.enabled", "true")

Step 5: ベーステーブルの作成

まず、マテリアライズドビューの元となるIcebergテーブルを作成します。

# データベース作成
spark.sql("CREATE DATABASE IF NOT EXISTS glue_catalog.iceberg_mv")
spark.sql("USE glue_catalog.iceberg_mv")

# ベーステーブル作成
spark.sql("""
CREATE TABLE glue_catalog.iceberg_mv.orders (
    order_id INT,
    customer_name STRING,
    product_category STRING,
    amount DECIMAL(10, 2),
    quantity INT,
    order_date DATE,
    region STRING
)
USING iceberg
PARTITIONED BY (region)
""")

# サンプルデータ挿入
spark.sql("""
INSERT INTO {FULL_TABLE_NAME} VALUES
    (1, 'Tanaka Taro', 'Electronics', 150000.00, 1, DATE('2025-12-01'), 'Tokyo'),
    (2, 'Suzuki Hanako', 'Clothing', 25000.00, 3, DATE('2025-12-02'), 'Osaka'),
    (3, 'Yamamoto Ken', 'Electronics', 89000.00, 2, DATE('2025-12-03'), 'Tokyo'),
    (4, 'Sato Yuki', 'Books', 5500.00, 5, DATE('2025-12-04'), 'Fukuoka'),
    (5, 'Watanabe Akira', 'Electronics', 45000.00, 1, DATE('2025-12-05'), 'Tokyo'),
    (6, 'Takahashi Mika', 'Clothing', 18000.00, 2, DATE('2025-12-06'), 'Osaka'),
    (7, 'Ito Kenji', 'Books', 12000.00, 4, DATE('2025-12-07'), 'Sapporo'),
    (8, 'Nakamura Emi', 'Electronics', 200000.00, 1, DATE('2025-12-08'), 'Tokyo'),
    (9, 'Kobayashi Ryo', 'Clothing', 35000.00, 2, DATE('2025-12-09'), 'Osaka'),
    (10, 'Kato Ayumi', 'Books', 8500.00, 3, DATE('2025-12-10'), 'Sapporo')
""")

作成したリソースとサンプルデータの確認

--- 現在のカタログ ---
+-----------------+
|current_catalog()|
+-----------------+
|     glue_catalog|
+-----------------+

--- 現在のデータベース ---
+------------------+
|current_database()|
+------------------+
|        iceberg_mv|
+------------------+

--- 作成されたテーブル一覧 ---
+----------+---------+-----------+
| namespace|tableName|isTemporary|
+----------+---------+-----------+
|iceberg_mv|   orders|      false|
+----------+---------+-----------+

--- テーブル詳細 ---
+--------------------+-------------+-------+
|            col_name|    data_type|comment|
+--------------------+-------------+-------+
|            order_id|          int|   NULL|
|       customer_name|       string|   NULL|
|    product_category|       string|   NULL|
|              amount|decimal(10,2)|   NULL|
|            quantity|          int|   NULL|
|          order_date|         date|   NULL|
|              region|       string|   NULL|
|# Partition Infor...|             |       |
|          # col_name|    data_type|comment|
|              region|       string|   NULL|
+--------------------+-------------+-------+

サンプルデータを挿入しました。

--- ベーステーブルの内容 ---
+--------+--------------+----------------+---------+--------+----------+-------+
|order_id|customer_name |product_category|amount   |quantity|order_date|region |
+--------+--------------+----------------+---------+--------+----------+-------+
|1       |Tanaka Taro   |Electronics     |150000.00|1       |2025-12-01|Tokyo  |
|2       |Suzuki Hanako |Clothing        |25000.00 |3       |2025-12-02|Osaka  |
|3       |Yamamoto Ken  |Electronics     |89000.00 |2       |2025-12-03|Tokyo  |
|4       |Sato Yuki     |Books           |5500.00  |5       |2025-12-04|Fukuoka|
|5       |Watanabe Akira|Electronics     |45000.00 |1       |2025-12-05|Tokyo  |
|6       |Takahashi Mika|Clothing        |18000.00 |2       |2025-12-06|Osaka  |
|7       |Ito Kenji     |Books           |12000.00 |4       |2025-12-07|Sapporo|
|8       |Nakamura Emi  |Electronics     |200000.00|1       |2025-12-08|Tokyo  |
|9       |Kobayashi Ryo |Clothing        |35000.00 |2       |2025-12-09|Osaka  |
|10      |Kato Ayumi    |Books           |8500.00  |3       |2025-12-10|Sapporo|
+--------+--------------+----------------+---------+--------+----------+-------+

Step 6: マテリアライズドビューの作成

地域別・カテゴリ別の売上サマリーを集計するマテリアライズドビューを作成します。

spark.sql("""
CREATE MATERIALIZED VIEW glue_catalog.iceberg_mv.orders_summary_mv
AS SELECT
    region,
    product_category,
    COUNT(*) AS order_count,
    SUM(quantity) AS total_quantity,
    SUM(amount) AS total_amount,
    AVG(amount) AS avg_amount,
    MIN(order_date) AS first_order_date,
    MAX(order_date) AS last_order_date
FROM glue_catalog.iceberg_mv.orders
GROUP BY region, product_category
""")

Step 7: マテリアライズドビューの確認

作成したマテリアライズドビューの内容を確認します。

spark.sql("""
SELECT * FROM glue_catalog.iceberg_mv.orders_summary_mv
ORDER BY region, product_category
""").show()

実行結果

+-------+----------------+-----------+--------------+------------+-------------+----------------+---------------+
|region |product_category|order_count|total_quantity|total_amount|avg_amount   |first_order_date|last_order_date|
+-------+----------------+-----------+--------------+------------+-------------+----------------+---------------+
|Fukuoka|Books           |1          |5             |5500.00     |5500.000000  |2025-12-04      |2025-12-04     |
|Osaka  |Clothing        |3          |7             |78000.00    |26000.000000 |2025-12-02      |2025-12-09     |
|Sapporo|Books           |2          |7             |20500.00    |10250.000000 |2025-12-07      |2025-12-10     |
|Tokyo  |Electronics     |4          |5             |484000.00   |121000.000000|2025-12-01      |2025-12-08     |
+-------+----------------+-----------+--------------+------------+-------------+----------------+---------------+

Step 8: マテリアライズドビューのリフレッシュ

ベーステーブルにデータを追加した後、マテリアライズドビューをリフレッシュします。

# 新しいデータを挿入
spark.sql("""
INSERT INTO glue_catalog.iceberg_mv.orders VALUES
    (11, 'Morita Sota', 'Electronics', 320000.00, 1, DATE('2025-12-11'), 'Tokyo'),
    (12, 'Honda Sakura', 'Books', 15000.00, 6, DATE('2025-12-12'), 'Nagoya')
""")

# フルリフレッシュ実行
spark.sql("REFRESH MATERIALIZED VIEW glue_catalog.iceberg_mv.orders_summary_mv FULL")

リフレッシュ後の結果

+-------+----------------+-----------+--------------+------------+-------------+----------------+---------------+
|region |product_category|order_count|total_quantity|total_amount|avg_amount   |first_order_date|last_order_date|
+-------+----------------+-----------+--------------+------------+-------------+----------------+---------------+
|Fukuoka|Books           |1          |5             |5500.00     |5500.000000  |2025-12-04      |2025-12-04     |
|Nagoya |Books           |1          |6             |15000.00    |15000.000000 |2025-12-12      |2025-12-12     |
|Osaka  |Clothing        |3          |7             |78000.00    |26000.000000 |2025-12-02      |2025-12-09     |
|Sapporo|Books           |2          |7             |20500.00    |10250.000000 |2025-12-07      |2025-12-10     |
|Tokyo  |Electronics     |5          |6             |804000.00   |160800.000000|2025-12-01      |2025-12-11     |
+-------+----------------+-----------+--------------+------------+-------------+----------------+---------------+

Nagoya地域のデータが追加され、Tokyo地域のElectronicsの集計値が更新されていることが確認できます。

Step 9: テーブルプロパティの確認

マテリアライズドビューのプロパティを確認すると、MVであることを示す情報が確認できます。

spark.sql("SHOW TBLPROPERTIES glue_catalog.iceberg_mv.orders_summary_mv").show(truncate=False)

主なプロパティ

プロパティ 説明
isMaterializedView マテリアライズドビューかどうか(true)
lastRefreshType 最後のリフレッシュタイプ(FULL/INCREMENTAL)
viewOriginalText MVを定義したSQLクエリ
current-snapshot-id 現在のIcebergスナップショットID

ハマったポイント

1. Lake Formation権限エラー

lakeformation-enabled=true を設定した場合、以下のエラーが発生しました。

Error Category: PERMISSION_ERROR
Failed to retrieve AWS Lake Formation temporary credentials for table
arn:aws:glue:ap-northeast-1:123456789012:table/iceberg_mv/orders with permission ALL.
Error: AccessDeniedException

原因: 新規作成されたテーブルに対するLake Formationのテンポラリクレデンシャル取得に失敗

解決策: lakeformation-enabled=false に設定

2. SparkSessionの問題

元のスクリプトでは SparkSession.builder.getOrCreate() で新しいSparkSessionを作成していたため、GlueContextの認証コンテキストが継承されませんでした。

修正前(NG):

from pyspark.sql import SparkSession
spark = SparkSession.builder \
    .config("spark.sql.catalog.glue_catalog", "...") \
    .getOrCreate()  # 新しいセッションを作成 -> 認証コンテキストが継承されない

修正後(OK):

spark = glueContext.spark_session  # GlueContextのセッションを使用
spark.conf.set("spark.sql.catalog.glue_catalog", "...")  # 既存セッションに設定を追加

3. client.region設定の不足

spark.sql.catalog.glue_catalog.client.region を設定しないと、以下のエラーが発生します。

region must not be null

そのため、設定を追加しました。

spark.conf.set("spark.sql.catalog.glue_catalog.client.region", "ap-northeast-1")

実行時間

今回の検証では、G.1X × 2ワーカー × 1分14秒程度でした。

20251226-aws-glue-iceberg-mv-3

最後に

AWS Glue Data CatalogのApache Icebergベースのマテリアライズドビューを実際に実行するには、SparkSessionやJobパラメータの設定で試行錯誤しました。特にマテリアライズドビューを作成する権限不足に悩まされました。

その他には以下の設定をしました。

  • GlueContextのspark_sessionを使用し、新規SparkSessionを作成しないことが重要
  • client.region の設定を忘れないこと
  • Lake Formation連携は、現時点では lakeformation-enabled=false で動作

Apache Iceberg ベースのマテリアライズドビューの複雑な集計クエリを事前計算しておくことで、分析クエリのパフォーマンスを向上させることができます。更にAmazon Athena、Amazon EMR、AWS Glue の Apache Spark エンジンは自動クエリ書き換えに対応しており、ベーステーブルのクエリに対しても暗黙的にマテリアライズドビューのパフォーマンス改善を享受できます。

一方、本家Apache IcebergのMaterialized View仕様の検討中であり、こちらの機能も気になるところです。

https://bering.hatenadiary.com/entry/2025/12/16/083336

合わせて読みたい

https://aws.amazon.com/jp/blogs/news/introducing-apache-iceberg-materialized-views-in-aws-glue-data-catalog-2/

この記事をシェアする

FacebookHatena blogX

関連記事