11. 基本的な CRUD と既定値
samples/01_basic_crud.py は、テーブルの作成・追加・読み取り・削除・上書きをひととおり実行します。操作そのものは単純ですが、このサンプルの目的は既定値の確認にあります。
Iceberg の新機能(format version 3、deletion vector、merge-on-read)は仕様上は利用可能でも、テーブルを普通に作っただけでは有効になりません。何もしなければどの設定が使われるのかを、実際のテーブルから読み取ります。
確かめたいこと
- 新規テーブルの
format-versionの既定値 write.delete.modeの既定値- append 1 回でスナップショットがいくつ増えるか
deleteがどの operation として記録されるか- 条件つき
overwriteの結果
背景
format version は、テーブルが使えるメタデータ機能を決めます。v2 で row-level delete(position delete / equality delete ファイル)が導入され、v3 で deletion vector やデフォルト値つき列などが追加されました。ただしライブラリの既定値は最新版ではありません。PyIceberg では TableProperties.DEFAULT_FORMAT_VERSION が 2 で、明示しない限り v2 のテーブルができます。
write.delete.mode は、行削除をどう実装するかを決めるテーブルプロパティです。copy-on-write は削除対象を含むデータファイルを丸ごと書き直します。merge-on-read は削除マーカーだけを書き、読み取り時に適用します。既定は copy-on-write で、format version を v3 に上げても deletion vector が自動的に使われるわけではありません。PyIceberg の書き込みパスは merge-on-read を指定しても警告を出して copy-on-write に落ちます(samples/07_pitfalls.py で確認できます)。
もう 1 点、コミットとスナップショットの関係があります。Iceberg のコミットは、新しいメタデータ JSON を書き、カタログ側でポインタを原子的に差し替えることで成立します。1 回のコミットが 1 個のスナップショットと 1 個のメタデータ JSON を生むのは、実装の都合ではなく原子性を得るための構造です。高頻度コミットがメタデータの肥大を招くのはこのためです。
処理の流れ
lab.tripsを作り直します。スキーマはtrip_id(required)、city(required)、fare、ts(required)の 4 列です。作成直後のテーブルから既定値を読みます。
fmt = tbl.metadata.format_version
assert fmt == 2, f"期待は 2 でしたが {fmt} でした"
delete_mode = tbl.properties.get("write.delete.mode", "copy-on-write (未設定時の既定)")- 5 行を append し、スナップショット数と summary を表示します。ここで
dict(snap.summary)は使えません。SummaryはMappingを継承していますが__iter__がキーではなくタプルを返すため、dict()がAttributeError: 'tuple' object has no attribute 'lower'で落ちます。公開プロパティのadditional_propertiesを使います。
for k, v in sorted(snap.summary.additional_properties.items()):
print(f" {k} = {v}")- 全件読み取りと、述語プッシュダウンつきの読み取りを行います。
df = tbl.scan(row_filter="city == 'tokyo'", selected_fields=("trip_id", "fare")).to_arrow()もう 1 回 append し、スナップショットが増えることを確認します。
delete_filter="city == 'kyoto'"で削除し、operation を確認します。overwrite_filter="city == 'osaka'"つきのoverwriteで、条件に一致する行を置き換えます。
実測結果
作成直後の既定値は次のとおりでした。
--- 新規テーブルの既定値を確認します
format-version = 2
→ 既定は v2 です。v3 ではありません。
(PyIceberg の TableProperties.DEFAULT_FORMAT_VERSION = 2)
write.delete.mode = copy-on-write (未設定時の既定)
write.delete.mode はテーブルプロパティとして設定されておらず、get() のフォールバック文字列が表示されています。つまり明示的な値は書かれておらず、読み手側が仕様の既定である copy-on-write として解釈します。
5 行を append した結果です。
--- データを append します
スナップショット数: 1
現在のスナップショット: id=6414712164395274846
operation = Operation.APPEND
added-data-files = 1
added-files-size = 1780
added-records = 5
total-data-files = 1
total-delete-files = 0
total-equality-deletes = 0
total-files-size = 1780
total-position-deletes = 0
total-records = 5
summary には追加分(added-*)と累計(total-*)の両方が入ります。delete ファイル系のカウンタはすべて 0 で、この時点では row-level delete が存在しません。
述語プッシュダウンつきの読み取りでは、city == 'tokyo' の 2 行のみ、指定した 2 列だけが返りました。
trip_id fare
1 1200.0
3 2300.0
row_filter は列統計を使ったファイルプルーニングに使われます。このサンプルではデータファイルが 1 つしかないため枝刈りの効果は見えません。
2 回目の append 後は次のとおりです。
--- もう1回 append して、スナップショットが増えることを確認します
スナップショット数: 2
行数: 6
1 行の追加でスナップショットが 1 個増えました。
削除の結果です。
--- delete (copy-on-write で実行されます)
行数: 5
operation = Operation.OVERWRITE
kyoto の 1 行が消えて 6 行から 5 行になりましたが、記録された operation は DELETE ではなく OVERWRITE です。copy-on-write では削除対象を含むデータファイルを、その行を除いた内容で書き直します。ファイルの置き換えが起きているので、意味的には上書きにあたります。summary の operation が DELETE になるのは、データファイルを丸ごと削除できる場合(メタデータのみで完結する削除)です。
条件つき overwrite の結果です。
trip_id city fare ts
1 tokyo 1200.0 2026-07-01 01:00:00
2 osaka 8888.0 2026-07-01 02:00:00
3 tokyo 2300.0 2026-07-01 03:00:00
6 nagoya 1100.0 2026-07-03 09:00:00
city == 'osaka' に一致していた 2 行(trip_id 2 と 5)が、渡した 1 行(trip_id 2、fare 8888.0)に置き換わりました。条件に一致する行はすべて消え、渡したデータだけが残ります。UPSERT ではない点に注意が必要です。
ここから分かること
format version と delete mode の既定は、いずれも保守的な側に倒れています。v3 の機能や merge-on-read を使いたい場合は明示的な指定が要ります。「Iceberg は deletion vector に対応した」という記述と、手元のテーブルが実際に使っている構成は別の話です。既定のままなら v2 + copy-on-write です。
copy-on-write の削除は、対象行が少なくてもファイル単位の書き直しを伴います。1 行消すために数百 MB のファイルを書き直すことが起こり得るので、削除の頻度が高いワークロードでは書き込み量を見積もる必要があります。
コミット 1 回につきスナップショット 1 個とメタデータ JSON 1 個が増えるため、小さな書き込みを繰り返すとメタデータが線形に増えます。スナップショットの整理(expire)とデータファイルの整理は、運用に入る前に方針を決めておく対象です。スナップショットの寿命については 14. タイムトラベルと branch / tag で扱います。
前は 10. 接続とケイパビリティ確認、次は 12. スキーマ進化と field ID です。