90. 分かったこと

実験を通して確認できたことを集約します。各項目の詳細は、対応する実験のページを参照してください。

すべて 2026-07-17 時点Apache Iceberg 1.11.0 / PyIceberg 0.11.1 / Apache Polaris 1.6.0 での観測です。

裏が取れたこと

文献で読んだ内容を、実際に動かして確認できたものです。

主張 観測結果 詳細
新規テーブルの既定は format-version 2 format_version = 2 を確認 11
削除の既定は copy-on-write delete 後に delete ファイルが 0 件、データファイルが書き直された 11 17
rename しても field ID は変わらない rename 前後で field ID が一致 12
パーティション進化は既存データを書き換えない 新旧 2 つの spec_id がファイル単位で共存 13
hidden partitioning はパーティション列を書かせない 論理列 ts の述語だけで正しく絞り込めた(枝刈りの効き自体は本規模では非観測) 13
タグはスナップショットの寿命を延ばす 参照されたスナップショットが保持される 14
コミットはポインタのアトミックな差し替え metadata-location が新ファイルに切り替わる 15 16
credential vending が動作する テーブル単位にスコープされた短命の資格情報が払い出された 16
requirements 不一致は 409 になる 意図的に古い snapshot-id を表明して 409 を再現 16
PyIceberg は v3 を書けない ValueError: Unsupported table format version: 3 で拒否(書き込み直前の NotImplementedError は手前のガードのため到達せず) 17
merge-on-read は copy-on-write に落ちる 警告が出て delete ファイルが作られない 17

動かして初めて分かったこと

文献からは読み取れず、実際に構築する過程で判明したものです。

S3 互換ストレージでは roleArn を設定してはいけない

カタログの storage config に roleArn を書くと、コミット時に次で失敗します。

pyiceberg.exceptions.CommitStateUnknownException:
  StsException: (Service: Sts, Status Code: 400, Request ID: null)

roleArn があると Polaris は credential vending のために STS AssumeRole を呼びますが、RustFS も MinIO も STS に対応していません。省略すれば静的な資格情報が使われ、credential vending も機能します。

このエラーには紛らわしさがあります。CommitStateUnknownException は仕様上「コミットが成功したか失敗したか分からない」という重大な状態を指しますが、この場合の実態は単なる設定ミスです。エラーの型名から深刻度を判断すると誤ります。

s3.path-style-access は PyIceberg に存在せず、黙って無視される

このキーは Java 版 Iceberg / Spark のものです。PyIceberg に書いてもエラーにならず無視されます。プロパティが単なる dict 参照のため、未知のキーが検出されないからです。

さらに、path-style を使いたいなら何も設定しなくて構いません。PyArrow の S3FileSystemforce_virtual_addressing の既定が False で、endpoint_override が設定されていれば path-style になります。つまり s3.endpoint を設定した時点で目的は達成されています。

同じ compose の中で Java 側は CATALOG_S3_PATH__STYLE__ACCESS=true が有効、Python 側は同名概念のキーが存在しない、という非対称が生じます。ここは混乱しやすい箇所です。

スコープでロールを絞っても、権限は絞られなかった

Polaris は OAuth2 の scope を「どの principal role を有効化するか」の指定として使います。PRINCIPAL_ROLE:ALL が全ロール、PRINCIPAL_ROLE:<名前> が個別指定です。

実装(polaris-runtime-service-1.6.0.jar を逆アセンブルして確認)では、トークン発行時に書式だけを検証し、ロールの実在は確認しません。認証時にはロール名で principal のロールを絞り込み、見つからなければ警告を記録して空集合のまま続行します。

問題は、有効ロールが空のトークンでもテーブルを新規作成できたことです。ログには roles=[] と警告が出ているにもかかわらず、createTable は HTTP 200 を返しました。purgeRequested=true 付きの削除は 403 になったので、認可が完全に無効というわけではありません。

スコープを権限の絞り込み手段として当てにしないほうが安全です。なぜ空のロール集合で書き込みが通るのかは追えていません(未確認)。なお確認したのは、仕様上 DEPRECATED for REMOVAL とされている /v1/oauth/tokens 経由の挙動です。推奨される外部 IdP 構成での挙動は確認していません。詳細は 01. 環境構成 を参照してください。

型名が実態を表さないことがある

上の CommitStateUnknownException に加えて、409 にも 2 つの意味があります。

状況 意味 リトライ
namespace 作成での 409 既に存在する 不可(リトライしても同じ)
updateTable での 409 コミット競合 可能(再ロードして再コミット)

同じステータスコードでも対処が正反対です。詳しくは 16. REST API を直接叩く を参照してください。

実装の成熟度について確認できたこと

PyIceberg 0.11.1 で「できないこと」を実際に踏んで確認しました。これらは欠陥ではなく成熟度の問題ですが、知らずに設計すると行き詰まります。

できないこと 実態
format-version 3 の書き込み 型は実在しパースは可能。アップグレードは ValueError で拒否される(NotImplementedError のガードもあるが手前で弾かれ到達しない)。deletion vector も row lineage も使えません
equality delete の読み取り 書けないだけでなく読めません。Flink の UPSERT は equality delete ベースなので、その組み合わせは実害が出ます
merge-on-read での書き込み プロパティを設定しても警告を出して copy-on-write に落ちます
compaction / orphan file 削除 MaintenanceTableexpire_snapshots のみ。小ファイル問題を PyIceberg 単体で解決できません
MERGE INTO 相当 upsert() は真偽値 2 つのみ。条件付き句も WHEN MATCHED THEN DELETE もありません

ここから導かれる現実的な構成は、PyIceberg で読み取りと軽い書き込み、メンテナンスと重い DML は Spark に任せるというものです。「Python だけで Iceberg を運用する」は現時点で成立しません。

設計上、注意が必要だと分かったこと

コミット 1 回がメタデータ 1 世代を生む

append を 1 回行うたびにスナップショットが 1 つ、metadata JSON が 1 つ増えます。これは最適化の余地ではなく、原子性を実現するための構造的な要件です。高頻度コミットがメタデータ肥大を招くのは、この構造の直接の帰結です。

あわせて既定値に注意が要ります。

  • write.metadata.previous-versions-max の既定は 100
  • write.metadata.delete-after-commit.enabled の既定は false

つまり何もしないと metadata JSON は溜まり続けます。しかも 100 個を溢れた分は追跡対象から外れ、後から削除を有効化しても消せません。

タグを 1 つ打つと自動メンテナンスが止まることがある

タグやブランチから参照されているスナップショットは削除されません。これは仕様どおりの挙動です。

問題は、マネージドサービスでの扱いです。Amazon S3 Tables の公式ドキュメントによれば、ユーザー定義のタグやブランチが存在すると snapshot management がテーブル全体で失敗します。タグを 1 つ打っただけで自動メンテナンスが全面停止し、それが課金の増加としてしか現れません。

統計の既定値がプルーニングを効かなくすることがある

  • write.metadata.metrics.default = truncate(16) — string の境界値は 16 文字で切り詰められます。URL や UUID のように共通プレフィックスが長い列では境界値が実質同一になり、file skipping が効きません。
  • write.metadata.metrics.max-inferred-column-defaults = 100 — 101 列目以降には既定でメトリクスが収集されません。ワイドテーブルで後方の列に述語をかけると全ファイル読みになります。

どちらも「遅い」という形でしか表面化せず、原因にたどり着きにくい部類です。

このラボで確かめられなかったこと

範囲外だったものを明示しておきます。

対象 理由
性能・スケール特性 ローカル単一ノードでは意味のある測定ができません
compaction の実際 Spark procedure が必要で、本ラボの構成では試せません
v3 機能(deletion vector / row lineage) PyIceberg が書けないため。Spark を繋げば検証できます
equality delete を含むテーブルの読み取り 書き手(Flink 等)が用意できないため未検証
複数エンジンからの同時コミット競合 409 は API を直接叩いて再現しましたが、実エンジン同士の競合は未検証
本番相当の認証(外部 IdP) /v1/oauth/tokens を使用。これは仕様上、削除予定のエンドポイントです

Spark を繋げば上の多くが検証できます。Polaris 公式の guides/spark が出発点になります。

総括

このラボで得られた実感は、Iceberg は仕様と実装の距離が大きいということです。

仕様は v3 で deletion vector を定義し、v4 が議論されています。一方 PyIceberg は v3 を書けず、Java 実装は仕様未採択の v4 を既に受け付けます。「Iceberg が対応している」という表現は、どの仕様バージョンの、どの実装の話かを伴わなければ意味を持ちません。

同時に、中核の設計は確かめた範囲では一貫していました。field ID による解決、manifest を書いた時点の spec で解釈する規則、ポインタ差し替えによるコミット — これらは互いに噛み合っており、スキーマ進化とパーティション進化が既存データを書き換えずに成立する理由になっています。ここは文献の説明どおりでした。

判断に使う際は、調査報告書とあわせて、必ず一次情報で裏を取ってください。