[アップデート] Amazon S3 Tables で Apache Iceberg V3 の全データ型が利用可能になったので AWS Glue 6.0 の Interactive Session で試してみた

[アップデート] Amazon S3 Tables で Apache Iceberg V3 の全データ型が利用可能になったので AWS Glue 6.0 の Interactive Session で試してみた

Amazon S3 Tables が Apache Iceberg V3 の新しいデータ型をサポートしました。AWS Glue 6.0 の Interactive Session を使い、ローカルの Jupyter ノートブックから geometry、geography、ナノ秒精度のタイムスタンプなどの V3 データ型を試してみました。
2026.10.12

クラウド事業統括本部の石川です。Amazon S3 Tables が Apache Iceberg V3 の geometry、geography、unknown、ナノ秒精度のタイムスタンプの各データ型と、列のデフォルト値をサポートしました。アドホックにクエリを実行できる AWS Glue 6.0 の Interactive Session を使い、ローカルの Jupyter ノートブックから試してみました。

https://aws.amazon.com/jp/about-aws/whats-new/2026/09/amazon-s3-tables-iceberg-v3-data-types/

Amazon S3 Tables が Apache Iceberg バージョン 3(V3)仕様で定義された geometry、geography、unknown、ナノ秒精度のタイムスタンプの各データ型と、列のデフォルト値をサポートしました。S3 Tables は以前から Variant 型、削除ベクトル(deletion vectors)、行リネージ(row lineage)をサポートしており、今回のリリースで V3 で導入されたデータ型がすべてそろいました。

発表によると、地理空間の座標やナノ秒精度のイベント時刻を、文字列や整数にエンコードせずにそのまま保存できます。列のデフォルト値を使うと、新しく追加した列について、既存の行の値を埋めるバックフィルが不要になります。対象は S3 Tables が提供されているすべての AWS リージョンです。

本記事では、ローカルの Jupyter から AWS Glue 6.0 の Interactive Session を起動し、S3 Tables の Iceberg REST endpoint に接続して、V3 の各データ型を Spark SQL で書き込み・読み取りしました。検証は 1 本のノートブック(38 セル)で行い、セルの出力を抜粋して掲載しています。

https://docs.aws.amazon.com/AmazonS3/latest/userguide/working-with-apache-iceberg-v3.html

https://aws.amazon.com/blogs/aws/amazon-s3-tables-now-support-all-apache-iceberg-v3-data-types/

Apache Iceberg V3 で追加されたデータ型とは

Apache Iceberg は、Amazon S3 などのオブジェクトストレージ上のデータファイルを、スキーマ進化やタイムトラベルができるテーブルとして扱うためのオープンテーブルフォーマットです。V3 は、そのテーブルフォーマット仕様のバージョン 3 です。

S3 のユーザーガイドに記載されている、今回 S3 Tables がサポートしたデータ型と機能は以下のとおりです。

種類 内容
geometry 点・線・ポリゴンを平面(デカルト座標)上の値として格納する
geography 点・線・ポリゴンを回転楕円体上の球面座標として格納する
ナノ秒精度のタイムスタンプ タイムゾーンなし(timestamp(9))とタイムゾーンあり(timestamptz(9))の 2 種類
unknown 型が未確定の列。optional でなければならず、常に null として読まれ、データファイルには書き込まれない。後から具体的な型に変更できる
列のデフォルト値 列を追加する前に書かれた行に使う initial-default と、書き込み時に列が省略された場合に使う write-default

geometry と geography は、空間参照系識別子(SRID)0 と 4326 を認識します。V3 仕様では、unknown、variant、geometry、geography の列のデフォルト値は null でなければならず、null 以外の initial-default / write-default は無効です。

V3 のデータ型は Parquet 形式のテーブルでのみ使え、ORC と Avro では使えません。また、V2 から V3 へのアップグレードは一方向で、V3 にしたテーブルを V2 に戻すことはできません。

AWS Glue については、S3 Tables のユーザーガイドの Glue 連携のページに、S3 Tables は Glue 5.0 以降、Iceberg V3 は Glue 5.1 以降、V3 のデータ型は Glue 6.0 以降が必要と記載されています。AWS Glue 6.0 は Spark 4.1.1、Python 3.13、Iceberg 1.11.0 をベースにしています。Spark で geometry と geography を使うには spark.sql.geospatial.enabled=true の設定が必要で、Glue では --conf 引数で渡します。variant、unknown、ナノ秒精度のタイムスタンプ、列のデフォルト値には追加の Spark 設定は不要です。S3 Tables の variant 型は提供リージョンが限定されていますが、アジアパシフィック(東京)は含まれています。

https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrating-glue.html

https://docs.aws.amazon.com/glue/latest/dg/release-notes.html

AWS Glue Interactive Session とは

AWS Glue Interactive Session は、AWS Glue のサーバーレスな Spark 実行環境をオンデマンドで起動し、Jupyter 互換のノートブックからコードを 1 セルずつ実行する機能です。AWS が公開している Jupyter カーネル(pip パッケージ aws-glue-sessions)をローカルにインストールすると、手元の Jupyter からセッションを起動できます。セッションの設定は %glue_version や %%configure などの magic コマンドで指定し、最初のコードセルを実行した時点でセッションが起動します。

課金はセッションがアクティブな時間と DPU 数に対して 1 秒単位で発生します。既定は 5 DPU、最小は 2 DPU で、1 セッションあたり 1 分の最低課金があります。セッションは、セルの実行後に %idle_timeout で指定した時間(Spark ETL セッションの既定値は 2,880 分)操作がないとタイムアウトしますので安心して利用できます。使い終わったセッションは %stop_session で停止できます。

https://docs.aws.amazon.com/glue/latest/dg/interactive-sessions-magics.html

https://docs.aws.amazon.com/glue/latest/dg/interactive-sessions-session-pricing.html

やってみた

検証構成

ローカル Mac の Jupyter から Glue 6.0 の Interactive Session を起動し、セッション上の Spark が S3 Tables の Iceberg REST endpoint に SigV4 で接続する構成です。metadata.json の確認には、セッション上の boto3 から S3 Tables の API を呼び出しました。

前提条件

  • AWS リージョン: アジアパシフィック(東京)ap-northeast-1
  • S3 Tables のテーブルバケット(blog-v3types-notebook)が作成済みであること
  • AWS Glue: 6.0(Spark 4.1.1-amzn-0、Iceberg 1.11.0-amzn-0)、ワーカーは G.1X × 2
  • ローカル環境: Python 3.11、aws-glue-sessions 1.0.9(Jupyter カーネル glue_pyspark)、nbclient 0.11.0
  • Interactive Session 用の IAM ロール(後述)と、ローカルの実行ユーザーにそのロールへの iam:PassRole 権限があること

ノートブックは Jupyter の画面から手で実行する代わりに、nbclient で先頭から順にヘッドレス実行し、出力付きの .ipynb として保存しました。以降に掲載する出力は、その .ipynb から抜粋したものです。アカウント ID などは置き換えています。

Interactive Session 用の IAM ロールを作成する

Glue が AssumeRole できる以下の信頼ポリシーでロールを作成し、AWS 管理ポリシー AWSGlueServiceRole をアタッチしました。

{
  "Version": "2012-10-17",
  "Statement": [
    {"Effect": "Allow", "Principal": {"Service": "glue.amazonaws.com"}, "Action": "sts:AssumeRole"}
  ]
}

S3 Tables へのアクセスは、テーブルバケットに限定した以下のインラインポリシーで許可しました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "S3TablesTableBucket",
      "Effect": "Allow",
      "Action": "s3tables:*",
      "Resource": [
        "arn:aws:s3tables:ap-northeast-1:123456789012:bucket/blog-v3types-notebook",
        "arn:aws:s3tables:ap-northeast-1:123456789012:bucket/blog-v3types-notebook/*"
      ]
    },
    {
      "Sid": "GlueSessionLogs",
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws-glue/*"
    }
  ]
}

セッションの設定を magic コマンドで指定する

ノートブックの最初のセルで、セッションの設定を magic コマンドで指定します。Glue のバージョンは 6.0、ワーカーは G.1X × 2、アイドルタイムアウトは 30 分にしました。%iam_role には前の手順で作成したロールの ARN を指定します。

%idle_timeout 30
%glue_version 6.0
%worker_type G.1X
%number_of_workers 2
%region ap-northeast-1
%iam_role arn:aws:iam::123456789012:role/blog-glue-is-6b1547cd
%session_id_prefix blog-v3types
Current idle_timeout is None minutes.
idle_timeout has been set to 30 minutes.
Setting Glue version to: 6.0
Previous worker type: None
Setting new worker type to: G.1X
Previous number of workers: None
Setting new number of workers to: 2
Previous region: ap-northeast-1
Setting new region to: ap-northeast-1
Region is set to: ap-northeast-1
Current iam_role is None
iam_role has been set to arn:aws:iam::123456789012:role/blog-glue-is-6b1547cd.
Setting session ID prefix to blog-v3types

次のセルでは、%%tags でセッションにタグを付けます。

%%tags
{"run_id": "blog-20261007-6b1547cd", "owner": "blog", "purpose": "blog-verification", "ttl": "2026-10-08"}
Tag {'run_id': 'blog-20261007-6b1547cd', 'owner': 'blog', 'purpose': 'blog-verification', 'ttl': '2026-10-08'} added

Spark の設定は %%configure で渡します。Glue ジョブのジョブパラメータと同じく、--datalake-formats と --conf をキーにした JSON を書きます。Iceberg の Spark 拡張、S3 Tables の Iceberg REST endpoint をカタログとする設定、spark.sql.geospatial.enabled=true、Spark SQL の列のデフォルト値の設定 spark.sql.defaultColumn.enabled=true、セッションのタイムゾーン(UTC)を、1 つの --conf の値に連結しています。デフォルトカタログを s3tables にしたため、以降の SQL ではカタログ名を省略して v3nb.<テーブル名> と書けます。

%%configure
{
  "--datalake-formats": "iceberg",
  "--conf": "spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions --conf spark.sql.geospatial.enabled=true --conf spark.sql.defaultColumn.enabled=true --conf spark.sql.session.timeZone=UTC --conf spark.sql.defaultCatalog=s3tables --conf spark.sql.catalog.s3tables=org.apache.iceberg.spark.SparkCatalog --conf spark.sql.catalog.s3tables.type=rest --conf spark.sql.catalog.s3tables.uri=https://s3tables.ap-northeast-1.amazonaws.com/iceberg --conf spark.sql.catalog.s3tables.warehouse=arn:aws:s3tables:ap-northeast-1:123456789012:bucket/blog-v3types-notebook --conf spark.sql.catalog.s3tables.rest.sigv4-enabled=true --conf spark.sql.catalog.s3tables.rest.signing-name=s3tables --conf spark.sql.catalog.s3tables.rest.signing-region=ap-northeast-1 --conf spark.sql.catalog.s3tables.io-impl=org.apache.iceberg.aws.s3.S3FileIO --conf spark.sql.catalog.s3tables.rest-metrics-reporting-enabled=false"
}

S3 Tables の Iceberg REST endpoint に接続するための設定項目は、S3 Tables のユーザーガイドにある Interactive Session 向けの %%configure の例と同じです。ユーザーガイドの例はキーを conf としていますが、今回は --conf で指定し、後述のとおりセッションに反映されることを確認しました。

セッションを起動してバージョンを確認する

最初のコードセルで SparkSession を取得し、Spark と Iceberg のバージョン、%%configure で渡した設定を表示します。このセルの実行でセッションが起動します。

from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
print("spark.version =", spark.version)
try:
    print("iceberg.version =", spark._jvm.org.apache.iceberg.IcebergBuild.version())
except Exception as e:
    print("iceberg.version = (unavailable)", type(e).__name__)
for k in ["spark.sql.geospatial.enabled", "spark.sql.defaultColumn.enabled", "spark.sql.session.timeZone",
          "spark.sql.defaultCatalog", "spark.sql.catalog.s3tables.type", "spark.sql.catalog.s3tables.uri",
          "spark.sql.catalog.s3tables.warehouse"]:
    print(f"{k} = {spark.conf.get(k, None)}")
Trying to create a Glue session for the kernel.
Session Type: etl
Worker Type: G.1X
Number of Workers: 2
Idle Timeout: 30
Session ID: blog-v3types-327c10eb-750f-488d-ba09-f781c14f077d
...
Waiting for session blog-v3types-327c10eb-750f-488d-ba09-f781c14f077d to get into ready status...
Session blog-v3types-327c10eb-750f-488d-ba09-f781c14f077d has been created.
spark.version = 4.1.1-amzn-0
iceberg.version = 1.11.0-amzn-0
spark.sql.geospatial.enabled = true
spark.sql.defaultColumn.enabled = true
spark.sql.session.timeZone = UTC
spark.sql.defaultCatalog = s3tables
spark.sql.catalog.s3tables.type = rest
spark.sql.catalog.s3tables.uri = https://s3tables.ap-northeast-1.amazonaws.com/iceberg
spark.sql.catalog.s3tables.warehouse = arn:aws:s3tables:ap-northeast-1:123456789012:bucket/blog-v3types-notebook

このセルの実行には、セッションの作成と READY になるまでの待ち時間を含めて約 45 秒かかりました。Spark は 4.1.1-amzn-0、Iceberg は 1.11.0-amzn-0 で、%%configure の設定がセッションに反映されています。

%status でセッションの状態を確認しました。

%status
Session ID: blog-v3types-327c10eb-750f-488d-ba09-f781c14f077d
Status: READY
Role: arn:aws:iam::123456789012:role/blog-glue-is-6b1547cd
CreatedOn: 2026-10-07 07:05:28.716000+09:00
GlueVersion: 6.0
Session Type: glueetl
Idle Timeout: 30
Timeout: 2880
Tags: {'owner': 'AROAXXXXXXXXXXXXXXXXX:ishikawa', 'run_id': 'blog-20261007-6b1547cd', 'purpose': 'blog-verification', 'ttl': '2026-10-08'}
Worker Type: G.1X
Number of Workers: 2
Region: ap-northeast-1
...

run_id、purpose、ttl は %%tags で指定した値のまま付いていましたが、owner の値は指定した blog ではなく、AROAXXXXXXXXXXXXXXXXX:ishikawa(セッションを作成した IAM ロールの ID と、AssumeRole したときのロールセッション名)になっていました。owner 以外のキー名を使えば、指定した値をタグに残せます。

小さなヘルパー関数 q() の作成と namespace を作成する

SQL の実行には、SQL 文を受け取って実行し、結果またはエラーを表示する小さなヘルパー関数 q() を使いました。失敗が想定される SQL には expect_error=True を渡し、エラーが返っても後続のセルを実行できるようにしています。

NS = "v3nb"

def q(sql, show=True, n=50, expect_error=False):
    """Run one SQL statement, print the statement, the result (or the error), and a RESULT marker."""
    print(">>> " + sql.strip())
    try:
        df = spark.sql(sql)
        if show:
            df.show(n, truncate=False)
        print("RESULT: " + ("UNEXPECTED_SUCCESS" if expect_error else "OK"))
        return df
    except Exception as e:
        msg = str(e)
        print(f"{type(e).__name__}: {msg[:1500]}")
        print("RESULT: " + ("EXPECTED_ERROR" if expect_error else "ERROR"))
        return None

q(f"CREATE NAMESPACE IF NOT EXISTS {NS}", show=False)
q("SHOW NAMESPACES")
+---------+
|namespace|
+---------+
|v3nb     |
+---------+

Spark SQL の CREATE NAMESPACE で、テーブルバケットに namespace v3nb を作成できました。以降は SQL 文と結果だけを掲載し、q() が出力する SQL 文の行と RESULT: の行は省略します。

geometry と geography を試す

geometry 列と geography 列、ナノ秒精度のタイムスタンプ列を持つ V3 テーブルを作成しました。V3 テーブルにするには、テーブルプロパティ format-version に 3 を指定します。

CREATE TABLE v3nb.vehicle_telemetry (
  event_id     string,
  vehicle_id   string,
  event_time   timestamp_ntz(9),
  position     geometry(4326),
  service_area geography(4326)
)
USING iceberg
TBLPROPERTIES ('format-version' = '3');

DESCRIBE TABLE v3nb.vehicle_telemetry;
SHOW TBLPROPERTIES v3nb.vehicle_telemetry;
+------------+----------------+-------+
|col_name    |data_type       |comment|
+------------+----------------+-------+
|event_id    |string          |NULL   |
|vehicle_id  |string          |NULL   |
|event_time  |timestamp_ntz(9)|NULL   |
|position    |geometry(4326)  |NULL   |
|service_area|geography(4326) |NULL   |
+------------+----------------+-------+

+-------------------------------+---------------+
|key                            |value          |
+-------------------------------+---------------+
|current-snapshot-id            |none           |
|format                         |iceberg/parquet|
|format-version                 |3              |
|write.parquet.compression-codec|zstd           |
+-------------------------------+---------------+

テーブルは作成でき、DESCRIBE には指定した型がそのまま表示されました。format-version は 3 です。指定していない write.parquet.compression-codec には zstd が設定されていました。

S3 のユーザーガイドの例と同じく、geography の値を ST_GeogFromWKT で作って INSERT してみました。

INSERT INTO v3nb.vehicle_telemetry VALUES
  ('EVT-001', 'VAN-01',
   CAST('2026-10-07 09:00:00.123456789' AS TIMESTAMP_NTZ(9)),
   ST_SetSrid(ST_GeomFromWKT('POINT (139.7671 35.6812)'), 4326),
   ST_SetSrid(ST_GeogFromWKT('POLYGON ((139.76 35.675, 139.775 35.675, 139.775 35.69, 139.76 35.69, 139.76 35.675))'), 4326))
AnalysisException: [UNRESOLVED_ROUTINE] Cannot resolve routine `ST_GeogFromWKT` on search path [`system`.`builtin`, `system`.`session`, `s3tables`]. SQLSTATE: 42883; line 6 pos 14

ST_GeogFromWKT という関数は見つからないというエラーになりました。Glue 6.0 の Spark で使える空間関数を一覧表示してみます。

SHOW FUNCTIONS LIKE 'st_*'
+---------------+
|function       |
+---------------+
|st_area        |
|st_asbinary    |
|st_collect     |
|st_convexhull  |
|st_distance    |
|st_dwithin     |
|st_geogfromwkb |
|st_geomfromtext|
|st_geomfromwkb |
|st_geomfromwkt |
|st_intersection|
|st_intersects  |
|st_length      |
|st_makeline    |
|st_setsrid     |
|st_srid        |
|st_within      |
|st_x           |
|st_y           |
+---------------+

19 個の関数があり、geometry 用には WKT から値を作る st_geomfromwkt がありますが、geography 用は WKB から値を作る st_geogfromwkb だけでした。値を WKT の文字列に変換する st_astext もありません。

そこで、東京駅周辺のポリゴンを WKB(16 進リテラル)に変換し、ST_GeogFromWKB に渡して INSERT しました。geometry の点は ST_GeomFromWKT で作成しています。3 行目(EVT-003)は新宿駅付近の点で、geography 列は NULL にしました。

INSERT INTO v3nb.vehicle_telemetry VALUES
  ('EVT-001', 'VAN-01',
   CAST('2026-10-07 09:00:00.123456789' AS TIMESTAMP_NTZ(9)),
   ST_SetSrid(ST_GeomFromWKT('POINT (139.7671 35.6812)'), 4326),
   ST_SetSrid(ST_GeogFromWKB(X'01030000000100000005000000B81E85EB517861406666666666D64140CDCCCCCCCC7861406666666666D64140CDCCCCCCCC786140B81E85EB51D84140B81E85EB51786140B81E85EB51D84140B81E85EB517861406666666666D64140'), 4326)),
  ('EVT-002', 'BIKE-07',
   CAST('2026-10-07 09:00:00.123456790' AS TIMESTAMP_NTZ(9)),
   ST_SetSrid(ST_GeomFromWKT('POINT (139.7700 35.6830)'), 4326),
   ST_SetSrid(ST_GeogFromWKB(X'01030000000100000005000000B81E85EB517861406666666666D64140CDCCCCCCCC7861406666666666D64140CDCCCCCCCC786140B81E85EB51D84140B81E85EB51786140B81E85EB51D84140B81E85EB517861406666666666D64140'), 4326)),
  ('EVT-003', 'VAN-02',
   CAST('2026-10-07 09:00:01.000000001' AS TIMESTAMP_NTZ(9)),
   ST_SetSrid(ST_GeomFromWKT('POINT (139.7003 35.6896)'), 4326),
   NULL);

SELECT event_id,
       ST_Srid(position)           AS position_srid,
       ST_X(position)              AS lon,
       ST_Y(position)              AS lat,
       ST_Srid(service_area)       AS area_srid,
       hex(ST_AsBinary(position))  AS position_wkb
FROM v3nb.vehicle_telemetry
ORDER BY event_id;
+--------+-------------+--------+-------+---------+------------------------------------------+
|event_id|position_srid|lon     |lat    |area_srid|position_wkb                              |
+--------+-------------+--------+-------+---------+------------------------------------------+
|EVT-001 |4326         |139.7671|35.6812|4326     |01010000005F984C158C7861408104C58F31D74140|
|EVT-002 |4326         |139.77  |35.683 |4326     |0101000000713D0AD7A37861408195438B6CD74140|
|EVT-003 |4326         |139.7003|35.6896|NULL     |010100000071AC8BDB687661402AA913D044D84140|
+--------+-------------+--------+-------+---------+------------------------------------------+

INSERT は成功しました。ST_Srid は geometry 列、geography 列ともに 4326 を返し、ST_X / ST_Y で経度と緯度、ST_AsBinary で WKB を取り出せました。

次に、ST_Intersects で東京駅周辺のポリゴンに含まれる車両を抽出しました。まずは geometry 列に対して実行します。

SELECT event_id, vehicle_id
FROM v3nb.vehicle_telemetry
WHERE ST_Intersects(position,
        ST_SetSrid(ST_GeomFromWKT('POLYGON ((139.76 35.675, 139.775 35.675, 139.775 35.69, 139.76 35.69, 139.76 35.675))'), 4326))
ORDER BY event_id
+--------+----------+
|event_id|vehicle_id|
+--------+----------+
|EVT-001 |VAN-01    |
|EVT-002 |BIKE-07   |
+--------+----------+

ポリゴン内にある EVT-001 と EVT-002 が返り、新宿駅付近の EVT-003 は除外されました。同じ判定を geography 列に対して実行してみます。

SELECT event_id
FROM v3nb.vehicle_telemetry
WHERE ST_Intersects(service_area,
        ST_SetSrid(ST_GeogFromWKB(X'01010000005F984C158C7861408104C58F31D74140'), 4326))
AnalysisException: [DATATYPE_MISMATCH.ST_INTERSECTS_GEOGRAPHY_NOT_SUPPORTED] Cannot resolve "st_intersects(service_area, st_setsrid(st_geogfromwkb(X'01010000005F984C158C7861408104C58F31D74140'), 4326))" due to data type mismatch: `st_intersects` does not support GEOGRAPHY type inputs. Only GEOMETRY is supported. SQLSTATE: 42K09; line 4 pos 6;

st_intersects は GEOGRAPHY 型の入力をサポートしておらず、GEOMETRY 型のみ対応しているというエラーになりました。

ナノ秒精度のタイムスタンプを試す

Spark SQL のタイムスタンプ型の名前には、timestamp_ntz、timestamp_ltz、timestamp の 3 つがあります。この 3 つの型名にそれぞれ精度 9(ナノ秒)を付けた列を持つテーブルを作成し、1 ナノ秒差の行を INSERT しました。

CREATE TABLE v3nb.trades (
  trade_id     bigint,
  executed_ntz timestamp_ntz(9),
  executed_ltz timestamp_ltz(9),
  executed_ts  timestamp(9),
  symbol       string,
  price        decimal(18,8)
)
USING iceberg
TBLPROPERTIES ('format-version' = '3');

DESCRIBE TABLE v3nb.trades;
+------------+----------------+-------+
|col_name    |data_type       |comment|
+------------+----------------+-------+
|trade_id    |bigint          |NULL   |
|executed_ntz|timestamp_ntz(9)|NULL   |
|executed_ltz|timestamp(9)    |NULL   |
|executed_ts |timestamp(9)    |NULL   |
|symbol      |string          |NULL   |
|price       |decimal(18,8)   |NULL   |
+------------+----------------+-------+

timestamp_ltz(9) で作成した列は、DESCRIBE では timestamp(9) と表示されました。3 行を INSERT し、各列を文字列に変換して表示します。ntz_us は、ナノ秒精度の列をマイクロ秒精度の TIMESTAMP_NTZ に変換した値です。

INSERT INTO v3nb.trades VALUES
  (1, CAST('2026-10-07 09:00:00.123456789' AS TIMESTAMP_NTZ(9)), CAST('2026-10-07 09:00:00.123456789' AS TIMESTAMP_LTZ(9)), CAST('2026-10-07 09:00:00.123456789' AS TIMESTAMP(9)), 'AAPL', 189.12345678),
  (2, CAST('2026-10-07 09:00:00.123456790' AS TIMESTAMP_NTZ(9)), CAST('2026-10-07 09:00:00.123456790' AS TIMESTAMP_LTZ(9)), CAST('2026-10-07 09:00:00.123456790' AS TIMESTAMP(9)), 'AAPL', 189.12345679),
  (3, CAST('2026-10-07 09:00:01.000000001' AS TIMESTAMP_NTZ(9)), CAST('2026-10-07 09:00:01.000000001' AS TIMESTAMP_LTZ(9)), CAST('2026-10-07 09:00:01.000000001' AS TIMESTAMP(9)), 'MSFT', 415.00000001);

SELECT trade_id,
       CAST(executed_ntz AS STRING)                         AS ntz_ns,
       CAST(CAST(executed_ntz AS TIMESTAMP_NTZ) AS STRING)  AS ntz_us,
       CAST(executed_ltz AS STRING)                         AS ltz_ns,
       CAST(executed_ts AS STRING)                          AS ts_ns
FROM v3nb.trades
ORDER BY executed_ntz;
+--------+-----------------------------+--------------------------+-----------------------------+-----------------------------+
|trade_id|ntz_ns                       |ntz_us                    |ltz_ns                       |ts_ns                        |
+--------+-----------------------------+--------------------------+-----------------------------+-----------------------------+
|1       |2026-10-07 09:00:00.123456789|2026-10-07 09:00:00.123456|2026-10-07 09:00:00.123456789|2026-10-07 09:00:00.123456789|
|2       |2026-10-07 09:00:00.12345679 |2026-10-07 09:00:00.123456|2026-10-07 09:00:00.12345679 |2026-10-07 09:00:00.12345679 |
|3       |2026-10-07 09:00:01.000000001|2026-10-07 09:00:01       |2026-10-07 09:00:01.000000001|2026-10-07 09:00:01.000000001|
+--------+-----------------------------+--------------------------+-----------------------------+-----------------------------+

3 つの列はいずれもナノ秒の桁まで値を保持しており、.123456789 と .123456790 の 1 ナノ秒差の行を区別できました(文字列に変換すると末尾の 0 は表示されません)。一方、マイクロ秒精度に変換した ntz_us では、1 行目と 2 行目がどちらも .123456 になり、区別できなくなりました。

S3 のユーザーガイドの例では、タイムゾーンありのナノ秒精度のタイムスタンプに timestamptz(9) という型名を使っています。この型名で列を追加してみました。

ALTER TABLE v3nb.trades ADD COLUMN executed_tstz timestamptz(9)
ParseException:
[UNSUPPORTED_DATATYPE] Unsupported data type "TIMESTAMPTZ(9)". SQLSTATE: 0A000
== SQL (line 1, position 50) ==
...trades ADD COLUMN executed_tstz timestamptz(9)
                                   ^^^^^^^^^^^^^^

timestamptz(9) は Spark SQL ではサポートされていないデータ型として、構文解析の段階でエラーになりました。Glue 6.0 の Spark SQL では、timestamp_ntz(9)、timestamp_ltz(9)、timestamp(9) を使います。それぞれが Iceberg のどの型として記録されたかは、後述の metadata.json で確認します。

unknown 型を試す

Spark では、Iceberg の unknown 型に対応する型として void を指定します。void 型の列を持つテーブルを作成し、INSERT と SELECT を実行しました。

CREATE TABLE v3nb.staging_events (
  event_id bigint,
  payload  string,
  reserved void
)
USING iceberg
TBLPROPERTIES ('format-version' = '3');

DESCRIBE TABLE v3nb.staging_events;
INSERT INTO v3nb.staging_events VALUES (1, 'first', NULL), (2, 'second', NULL);
SELECT * FROM v3nb.staging_events ORDER BY event_id;
+--------+---------+-------+
|col_name|data_type|comment|
+--------+---------+-------+
|event_id|bigint   |NULL   |
|payload |string   |NULL   |
|reserved|void     |NULL   |
+--------+---------+-------+

+--------+-------+--------+
|event_id|payload|reserved|
+--------+-------+--------+
|1       |first  |NULL    |
|2       |second |NULL    |
+--------+-------+--------+

テーブルの作成、INSERT、SELECT はいずれも成功し、reserved 列は NULL として読まれました。

unknown 型は optional(NULL 許容)でなければならないため、NOT NULL を指定した場合の挙動も確認しました。

CREATE TABLE v3nb.neg_unknown_required (id int, reserved void NOT NULL)
USING iceberg TBLPROPERTIES ('format-version' = '3')
IllegalArgumentException: Cannot create required field with unknown type: reserved

unknown 型の必須フィールドは作成できないというエラーになり、V3 仕様のとおり拒否されました。

S3 のユーザーガイドには、unknown 型の列は後から具体的な型に変更できると記載されています。reserved 列を string 型に変更してみました。

ALTER TABLE v3nb.staging_events ALTER COLUMN reserved TYPE string;
DESCRIBE TABLE v3nb.staging_events;
Py4JJavaError: An error occurred while calling o295.sql.
: org.apache.iceberg.exceptions.BadRequestException: Malformed request: The specified metadata is not valid

+--------+---------+-------+
|col_name|data_type|comment|
+--------+---------+-------+
|event_id|bigint   |NULL   |
|payload |string   |NULL   |
|reserved|void     |NULL   |
+--------+---------+-------+

S3 Tables の Iceberg REST endpoint から BadRequestException: Malformed request: The specified metadata is not valid が返り、型は void のままでした。今回の構成では、unknown 型から string 型への変更は失敗しました。原因は特定できていません。

列のデフォルト値を試す

CREATE TABLE で currency 列に DEFAULT 'JPY' を宣言したテーブルを作成し、currency を省略した INSERT と、DEFAULT キーワードを使った INSERT を実行しました。セッションには spark.sql.defaultColumn.enabled=true を渡しています。

CREATE TABLE v3nb.orders (
  order_id bigint,
  amount   decimal(10,2),
  currency string DEFAULT 'JPY'
)
USING iceberg
TBLPROPERTIES ('format-version' = '3');

DESCRIBE TABLE v3nb.orders;
INSERT INTO v3nb.orders (order_id, amount) VALUES (1, 1000.00);
INSERT INTO v3nb.orders VALUES (2, 2000.00, DEFAULT);
SELECT * FROM v3nb.orders ORDER BY order_id;
+--------+-------------+-------+
|col_name|data_type    |comment|
+--------+-------------+-------+
|order_id|bigint       |NULL   |
|amount  |decimal(10,2)|NULL   |
|currency|string       |NULL   |
+--------+-------------+-------+

+--------+-------+--------+
|order_id|amount |currency|
+--------+-------+--------+
|1       |1000.00|NULL    |
|2       |2000.00|NULL    |
+--------+-------+--------+

CREATE TABLE と 2 つの INSERT はいずれも成功しましたが、currency はどちらの行も JPY ではなく NULL でした。

次に、ユーザーガイドの例と同じく、行があるテーブルに ADD COLUMN ... DEFAULT で列を追加しました。追加前の行と、追加後に列を省略して INSERT した行の値を確認します。

ALTER TABLE v3nb.orders ADD COLUMN channel string DEFAULT 'web';
SELECT * FROM v3nb.orders ORDER BY order_id;
INSERT INTO v3nb.orders (order_id, amount) VALUES (3, 3000.00);
SELECT * FROM v3nb.orders ORDER BY order_id;
+--------+-------+--------+-------+
|order_id|amount |currency|channel|
+--------+-------+--------+-------+
|1       |1000.00|NULL    |NULL   |
|2       |2000.00|NULL    |NULL   |
+--------+-------+--------+-------+

+--------+-------+--------+-------+
|order_id|amount |currency|channel|
+--------+-------+--------+-------+
|1       |1000.00|NULL    |NULL   |
|2       |2000.00|NULL    |NULL   |
|3       |3000.00|NULL    |NULL   |
+--------+-------+--------+-------+

ADD COLUMN は成功しましたが、追加前の行も、追加後に列を省略して INSERT した行も、channel は web ではなく NULL でした。続いて、既存の列のデフォルト値を変更してみました。

ALTER TABLE v3nb.orders ALTER COLUMN currency SET DEFAULT 'USD'
Py4JJavaError: An error occurred while calling o295.sql.
: org.apache.iceberg.exceptions.BadRequestException: Malformed request: The specified metadata is not valid

unknown 型の変更と同じエラーが S3 Tables の Iceberg REST endpoint から返りました。SHOW CREATE TABLE の出力にも DEFAULT 句はありません。

SHOW CREATE TABLE v3nb.orders
CREATE TABLE s3tables.v3nb.orders (
  order_id BIGINT,
  amount DECIMAL(10,2),
  currency STRING,
  channel STRING)
USING iceberg
LOCATION 's3://061b0bfb-8cad-4368-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--table-s3'
TBLPROPERTIES (
  'current-snapshot-id' = '707678058304584040',
  'format' = 'iceberg/parquet',
  'format-version' = '3',
  'write.parquet.compression-codec' = 'zstd')

デフォルト値がテーブルのメタデータに記録されていないかどうかは、後述の metadata.json で確認します。

variant 型を試す

variant 型の列を持つテーブルを作成し、S3 のユーザーガイドの例と同じ JSON を PARSE_JSON で INSERT しました。variant_get でフィールドを取り出し、to_json で variant の値全体を JSON 文字列に変換して表示します。

CREATE TABLE v3nb.events (
  event_id        bigint,
  event_timestamp timestamp,
  source          string,
  event_data      variant
)
USING iceberg
TBLPROPERTIES ('format-version' = '3');

INSERT INTO v3nb.events VALUES
  (1, TIMESTAMP '2026-10-07 09:00:00', 'web-app',
   PARSE_JSON('{"user_id": "u-1234", "action": "page_view", "page": "/products", "duration_ms": 350}')),
  (2, TIMESTAMP '2026-10-07 09:00:05', 'mobile-app',
   PARSE_JSON('{"user_id": "u-5678", "action": "purchase", "items": [{"sku": "A100", "qty": 2}], "total": 49.99}'));

SELECT event_id,
       source,
       variant_get(event_data, '$.user_id', 'string')  AS user_id,
       variant_get(event_data, '$.action', 'string')   AS action,
       variant_get(event_data, '$.total', 'double')    AS total,
       variant_get(event_data, '$.items[0].sku', 'string') AS first_sku,
       to_json(event_data)                             AS event_data_json
FROM v3nb.events
ORDER BY event_id;
+--------+----------+-------+---------+-----+---------+---------------------------------------------------------------------------------------+
|event_id|source    |user_id|action   |total|first_sku|event_data_json                                                                        |
+--------+----------+-------+---------+-----+---------+---------------------------------------------------------------------------------------+
|1       |web-app   |u-1234 |page_view|NULL |NULL     |{"action":"page_view","user_id":"u-1234"}                                              |
|2       |mobile-app|u-5678 |purchase |49.99|A100     |{"action":"purchase","items":[{"qty":2,"sku":"A100"}],"total":49.99,"user_id":"u-5678"}|
+--------+----------+-------+---------+-----+---------+---------------------------------------------------------------------------------------+

東京リージョンのテーブルバケットに variant 列を持つテーブルを作成でき、variant_get でネストした配列の要素($.items[0].sku)も含めてフィールドを型付きで取り出せました。1 行目の JSON には total と items がないため、total 列と first_sku 列は NULL です。

ただし、1 行目の to_json(event_data) の出力は {"action":"page_view","user_id":"u-1234"} で、INSERT した page と duration_ms が含まれていませんでした。欠けていた page と duration_ms は、どちらもこの SELECT の variant_get で参照していないフィールドでした。2 行目は、INSERT したフィールドがすべて出力されています。このノートブックでは原因の切り分けまでは行っていません。

Iceberg のメタデータテーブルと行リネージを確認する

Iceberg のメタデータテーブル .files と .snapshots で、各テーブルのデータファイルとスナップショットを確認しました。あわせて、V3 の行リネージで追加される _row_id と _last_updated_sequence_number を SELECT します。以下は vehicle_telemetry と orders の結果の抜粋です。

SELECT 'vehicle_telemetry' AS table_name, file_format, record_count, regexp_extract(file_path, '([^/]+)$', 1) AS file_name FROM v3nb.vehicle_telemetry.files;
SELECT 'orders' AS table_name, file_format, record_count, regexp_extract(file_path, '([^/]+)$', 1) AS file_name FROM v3nb.orders.files;
SELECT snapshot_id, operation, summary['added-records'] AS added_records FROM v3nb.orders.snapshots ORDER BY committed_at;
SELECT order_id, currency, channel, _row_id, _last_updated_sequence_number FROM v3nb.orders ORDER BY order_id;
+-----------------+-----------+------------+------------------------------------------------------------+
|table_name       |file_format|record_count|file_name                                                   |
+-----------------+-----------+------------+------------------------------------------------------------+
|vehicle_telemetry|PARQUET    |1           |00000-0-07d479d1-34e8-4a46-8c15-a95f88243e68-0-00001.parquet|
|vehicle_telemetry|PARQUET    |1           |00001-1-07d479d1-34e8-4a46-8c15-a95f88243e68-0-00001.parquet|
|vehicle_telemetry|PARQUET    |1           |00002-2-07d479d1-34e8-4a46-8c15-a95f88243e68-0-00001.parquet|
+-----------------+-----------+------------+------------------------------------------------------------+

+----------+-----------+------------+-------------------------------------------------------------+
|table_name|file_format|record_count|file_name                                                    |
+----------+-----------+------------+-------------------------------------------------------------+
|orders    |PARQUET    |1           |00000-16-c46e5736-cc72-4e11-b8b0-adeba37c83ad-0-00001.parquet|
|orders    |PARQUET    |1           |00000-13-7266da83-e3c8-4e1b-a79c-93080af86a7f-0-00001.parquet|
|orders    |PARQUET    |1           |00000-12-06b9bf0e-43e3-4402-a3dd-e75ed5c36cbb-0-00001.parquet|
+----------+-----------+------------+-------------------------------------------------------------+

+-------------------+---------+-------------+
|snapshot_id        |operation|added_records|
+-------------------+---------+-------------+
|4972217547182317256|append   |1            |
|7454704441310117738|append   |1            |
|707678058304584040 |append   |1            |
+-------------------+---------+-------------+

+--------+--------+-------+-------+-----------------------------+
|order_id|currency|channel|_row_id|_last_updated_sequence_number|
+--------+--------+-------+-------+-----------------------------+
|1       |NULL    |NULL   |0      |1                            |
|2       |NULL    |NULL   |1      |2                            |
|3       |NULL    |NULL   |2      |3                            |
+--------+--------+-------+-------+-----------------------------+

5 つのテーブルのデータファイルは、いずれも Parquet 形式でした。vehicle_telemetry は 1 回の INSERT で 3 行を書き込み、1 行ずつ 3 つのファイルに分かれています。orders には 3 回の INSERT に対応する append のスナップショットが 3 つあり、各行には _row_id が 0 から順に、_last_updated_sequence_number には書き込んだコミットの順に 1、2、3 が割り当てられていました。

metadata.json で型の記録を確認する

Spark の型名と Iceberg の型名の対応と、列のデフォルト値が記録されているかを確認するため、各テーブルの現在の metadata.json を読みました。セッションのロールの権限で、boto3 から S3 Tables の GetTableMetadataLocation を呼び出して metadata.json の場所を取得し、S3 の GetObject で内容を読み込んでいます。

import boto3, json
s3t = boto3.client("s3tables", region_name="ap-northeast-1")
s3 = boto3.client("s3", region_name="ap-northeast-1")
TB_ARN = "arn:aws:s3tables:ap-northeast-1:123456789012:bucket/blog-v3types-notebook"

def show_metadata(table):
    loc = s3t.get_table_metadata_location(tableBucketARN=TB_ARN, namespace=NS, name=table)
    ml = loc["metadataLocation"]
    print(f"== {table} ==")
    print("metadataLocation:", ml)
    bucket, key = ml[len("s3://"):].split("/", 1)
    try:
        body = s3.get_object(Bucket=bucket, Key=key)["Body"].read()
    except Exception as e:
        print("boto3 GetObject failed:", type(e).__name__, str(e)[:300])
        body = "\n".join(r.value for r in spark.read.text(ml).collect())
    md = json.loads(body)
    cur = [s for s in md["schemas"] if s["schema-id"] == md["current-schema-id"]][0]
    print("format-version:", md["format-version"], "| current-schema-id:", md["current-schema-id"], "| next-row-id:", md.get("next-row-id"))
    for f in cur["fields"]:
        print("  ", json.dumps(f, ensure_ascii=False))

for t in ["vehicle_telemetry", "trades", "staging_events", "orders", "events"]:
    try:
        show_metadata(t)
    except Exception as e:
        print(f"== {t} == ERROR {type(e).__name__}: {str(e)[:500]}")
== vehicle_telemetry ==
metadataLocation: s3://a9f8356d-5f45-4219-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--table-s3/metadata/00001-4cefd937-0891-4666-9434-3f8306251e2a.metadata.json
format-version: 3 | current-schema-id: 0 | next-row-id: 3
   {"id": 1, "name": "event_id", "required": false, "type": "string"}
   {"id": 2, "name": "vehicle_id", "required": false, "type": "string"}
   {"id": 3, "name": "event_time", "required": false, "type": "timestamp_ns"}
   {"id": 4, "name": "position", "required": false, "type": "geometry"}
   {"id": 5, "name": "service_area", "required": false, "type": "geography"}
== trades ==
metadataLocation: s3://e270ab28-b4fe-4dfb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--table-s3/metadata/00001-11779547-876e-481e-a6d2-34afb6474239.metadata.json
format-version: 3 | current-schema-id: 0 | next-row-id: 3
   {"id": 1, "name": "trade_id", "required": false, "type": "long"}
   {"id": 2, "name": "executed_ntz", "required": false, "type": "timestamp_ns"}
   {"id": 3, "name": "executed_ltz", "required": false, "type": "timestamptz_ns"}
   {"id": 4, "name": "executed_ts", "required": false, "type": "timestamptz_ns"}
   {"id": 5, "name": "symbol", "required": false, "type": "string"}
   {"id": 6, "name": "price", "required": false, "type": "decimal(18, 8)"}
== staging_events ==
metadataLocation: s3://6f94fe9c-031d-4095-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--table-s3/metadata/00001-d28332ba-9b6f-429a-8933-82b24ab46f51.metadata.json
format-version: 3 | current-schema-id: 0 | next-row-id: 2
   {"id": 1, "name": "event_id", "required": false, "type": "long"}
   {"id": 2, "name": "payload", "required": false, "type": "string"}
   {"id": 3, "name": "reserved", "required": false, "type": "unknown"}
== orders ==
metadataLocation: s3://061b0bfb-8cad-4368-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--table-s3/metadata/00004-dc46760f-a50f-4718-b2bf-850068f0ea41.metadata.json
format-version: 3 | current-schema-id: 1 | next-row-id: 3
   {"id": 1, "name": "order_id", "required": false, "type": "long"}
   {"id": 2, "name": "amount", "required": false, "type": "decimal(10, 2)"}
   {"id": 3, "name": "currency", "required": false, "type": "string"}
   {"id": 4, "name": "channel", "required": false, "type": "string"}
== events ==
metadataLocation: s3://77c43089-6be0-445f-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--table-s3/metadata/00001-33244a52-be24-4c18-a9aa-f59a0233bfb7.metadata.json
format-version: 3 | current-schema-id: 0 | next-row-id: 2
   {"id": 1, "name": "event_id", "required": false, "type": "long"}
   {"id": 2, "name": "event_timestamp", "required": false, "type": "timestamptz"}
   {"id": 3, "name": "source", "required": false, "type": "string"}
   {"id": 4, "name": "event_data", "required": false, "type": "variant"}

5 つのテーブルはいずれも format-version が 3 で、Spark の型名は Iceberg の型として以下のように記録されていました。

Spark SQL の型名 metadata.json の型
timestamp_ntz(9) timestamp_ns
timestamp_ltz(9)、timestamp(9) timestamptz_ns
timestamp(精度指定なし) timestamptz
geometry(4326) geometry
geography(4326) geography
void unknown
variant variant

orders の currency と channel のフィールドには、initial-default も write-default もありませんでした。CREATE TABLE と ADD COLUMN で宣言したデフォルト値は、DDL としてはエラーにならずに成功していますが、テーブルのメタデータには記録されていません。前の手順で読み取り値が NULL だったことと一致します。

next-row-id は、各テーブルに書き込んだ行数(3 行または 2 行)と同じ値でした。

考察

検証で確認できたことと、注意点を整理します。

  • Glue 6.0 では geography の値を WKB から作成した: Glue 6.0 の Spark(4.1.1-amzn-0)には ST_GeogFromWKT と ST_AsText がなく、SHOW FUNCTIONS に表示された geography の作成関数は ST_GeogFromWKB だけでした。S3 のユーザーガイドにある geography の INSERT の例はそのままでは実行できなかったため、ST_GeogFromWKB に WKB を渡して作成しました。AWS Big Data Blog の Glue 6.0 の例でも、geography の値は ST_GeogFromWKB で作成しています。geometry は ST_GeomFromWKT で作成でき、ST_X / ST_Y / ST_Srid / ST_AsBinary で値を確認できます。
  • ST_Intersects は geometry 列だけが対象: geography 列に対しては ST_INTERSECTS_GEOGRAPHY_NOT_SUPPORTED のエラーになりました。S3 のユーザーガイドにも、geography に対する測地的な空間述語はエンジンに依存し、Amazon EMR と AWS Glue の Spark の空間述語は geometry に対して動作すると記載されています。Glue 6.0 の Spark で ST_Intersects による位置のフィルタを行う列は、geometry 型にします。
  • タイムスタンプの型名はユーザーガイドの例と異なる: timestamptz(9) は Spark SQL の構文エラーになりました。タイムゾーンありのナノ秒精度のタイムスタンプには timestamp_ltz(9) または timestamp(9) を指定し、Iceberg には timestamptz_ns として記録されます。タイムゾーンなしは timestamp_ntz(9) で、timestamp_ns として記録されます。ユーザーガイドでは timestamp(9) をタイムゾーンなしと説明していますが、Glue 6.0 の Spark SQL で timestamp(9) と書いた列はタイムゾーンありの timestamptz_ns になりました。タイムゾーンなしの列を作る場合は、timestamp_ntz(9) と明示する必要があります。
  • ナノ秒精度はマイクロ秒精度の型に変換すると失われる: ナノ秒精度の列をマイクロ秒精度の TIMESTAMP_NTZ に変換すると、1 ナノ秒差の 2 行が同じ値になりました。ユーザーガイドにも、マイクロ秒精度にのみ対応したエンジンでナノ秒精度の列を読み書きすると値が切り捨てられると記載されています。
  • unknown 型の列は作成できたが、string 型への変更は失敗した: void 型の列は作成・INSERT・SELECT ができ、NOT NULL の指定は仕様どおり拒否されました。一方、ユーザーガイドに記載のある具体的な型への変更として試した ALTER COLUMN ... TYPE string は、S3 Tables の Iceberg REST endpoint が Malformed request: The specified metadata is not valid で拒否しました。原因は特定できていません。
  • 今回の構成では、列のデフォルト値がメタデータに記録されなかった: Glue 6.0 の Interactive Session から S3 Tables の Iceberg REST endpoint 経由で実行した CREATE TABLE と ADD COLUMN の DEFAULT は、DDL としては成功しましたが、metadata.json に initial-default / write-default が記録されず、読み取り値は NULL でした。SET DEFAULT は拒否されました。ユーザーガイドには S3 Tables で列のデフォルト値がサポートされると記載されていますが、今回の検証ではその動作を確認できず、原因も特定できていません。AWS Big Data Blog では、Glue 6.0 で Glue Data Catalog のテーブル(汎用 S3 バケット)に DEFAULT を宣言し、列を省略した INSERT でデフォルト値が入る例が紹介されています。今回は S3 Tables の Iceberg REST endpoint 経由で、カタログが異なります。同じ構成で列のデフォルト値を使う場合は、metadata.json に initial-default / write-default が記録されているか、読み取り値にデフォルト値が入るかを確認してから使うことをおすすめします。
  • variant 型は東京リージョンで使えた。to_json の出力は確認してから使う: variant 列の作成、PARSE_JSON による書き込み、variant_get による取り出しができました。一方、variant_get と同じ SELECT に書いた to_json の出力では、1 行目の page と duration_ms(いずれも variant_get で参照していないフィールド)が欠けていました。原因は切り分けていませんが、variant 列の全体を JSON 文字列として取り出す場合は、出力にフィールドの欠けがないかを確認してください。
  • 行リネージの列は追加の設定なしで使える: V3 テーブルでは _row_id と _last_updated_sequence_number を通常の列と同じように SELECT できました。ユーザーガイドでは、_last_updated_sequence_number を条件にして変更された行だけを処理する例が紹介されています。
  • V3 にする前に、テーブルを使うエンジンを確認する: ユーザーガイドによると、V2 から V3 へのアップグレードは元に戻せず、Amazon Athena は V3 テーブルを読めません。V3 のデータ型は Parquet 形式のテーブルだけで使え、variant、geometry、geography、ナノ秒精度のタイムスタンプの列は、ソートと Z-order のコンパクションのソート順に含められません。

今後、次の対応を期待しています。

  • Glue 6.0 から S3 Tables の Iceberg REST endpoint 経由で宣言した列のデフォルト値(initial-default / write-default)の記録と変更
  • 同じ構成での unknown 型から具体的な型への変更
  • Glue の Spark への ST_GeogFromWKT と ST_AsText の追加、geography 型に対する空間述語
  • Athena の Iceberg V3 対応

最後に

Amazon S3 Tables の Apache Iceberg V3 データ型のサポートを、AWS Glue 6.0 の Interactive Session とローカルの Jupyter ノートブックから試しました。当初は、Glue ETL 6.0 の ETL Job で試みましたが、想定外のことに迅速に対応できず、一週間以上手こずり、迷った末に Interactive Session に変更しました。

geometry、geography、ナノ秒精度のタイムスタンプ、unknown、variant の各データ型は、S3 Tables に作成した V3 テーブルで書き込みと読み取りができ、metadata.json にも V3 の型として記録されていました。geometry 列では ST_Intersects による範囲の判定、ナノ秒精度のタイムスタンプでは 1 ナノ秒差のイベントの区別、variant 列では variant_get によるフィールドの取り出しを、いずれも Spark SQL で行えました。

一方、Glue 6.0 では geography の値を WKB から作成し、タイムスタンプの型名もユーザーガイドの例とは異なるものを使いました。執筆時点の検証では、列のデフォルト値が metadata.json に記録されず、unknown 型から string 型への変更も失敗しました。また、V3 へのアップグレードは元に戻せず、Athena からは V3 テーブルを読めません。

Interactive Session を使うと、セッションの起動からこうした挙動の確認までを数分で行い、SQL と結果を 1 つのノートブックに残せます。既存のテーブルを V3 にする前に、使うエンジンと関数、型名をこの方法で確認しておくことをおすすめします。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事