TT Lab
はじめる
学ぶ 学習パス コース

レイクハウスのテーブル形式 — Apache Iceberg をメタデータで理解する

変更できないファイルで行を直す二つの方法 — copy-on-write と merge-on-read

TT Labで続きを見る

一言でいうと

Parquetファイルは書き換えられないので、1行を変えるには、そのファイルを新しく書き直すか(copy-on-write)、「どのファイルの何番目の行は削除された」という削除ファイルを別に書いておいて、読むときに取り除く(merge-on-read)必要があります。前者は書き込みが高くて読み取りが安く、後者はその反対です。テーブルプロパティ3つが、これを決めます。

なぜ問題になるのか: 100万行ファイルの1行

注文1件が返金に変わりました。その注文が入ったParquetファイルには、ほかの注文が100万件入っています。ファイルは1度書かれたら変わりません(仕様の要求条件が「インプレース書き込みなし」です)。そうすると、1件を直すために100万件を書き直すべきでしょうか。

フォーマットバージョン1では、そうでした。バージョン2が、この問題を解くために削除ファイルを加えました。不変のデータファイルを書き直さなくても、その中の個別の行を削除したり変更したりできるようになったのです。

どう動くのか: 削除の3つの形

行レベルの削除には、2つの系統があります。

種類 何で削除するか 主に誰が使うか
位置削除ファイル(バージョン2) (データファイルのパス、行の位置) Sparkのmerge-on-read
deletion vector(バージョン3以上) データファイルごとに1つのビットマップ バージョン3のテーブルの位置削除
等価削除ファイル 列の値(例: order_id = 'O123') ストリーミングupsert(Flinkなど)

位置削除ファイルは、file_pathとpos(0から数えた行番号)の2列のファイルで、file_path、続いてposの順に並べて書きます。読み取る側は、データファイルを読みながら、そのファイルの削除された位置を飛ばします。deletion vectorは、同じ情報をPuffinファイルの中のRoaringビットマップでより小さく収め、データファイルごとに最大1つだけ置きます。バージョン3では、位置削除ファイルが非推奨予定になりました。

等価削除ファイルは、位置を知らず、値だけを知っています。ストリーミングupsertは、古い行がどのファイルの何番目にあるかを探す余裕がないので、「このキーの古い行は削除せよ」とだけ書きます。その代わり、読み取る側が関連するすべてのデータファイルの行と値を照らし合わせなければならず、最もコストが高くなります。

どの削除がどのデータにかかるかは、シーケンス番号が決めます。スキャン計画のルールによると、位置削除はシーケンス番号が同じか小さいデータファイルに、等価削除は厳密に小さいデータファイルにだけかかります。そのため、等価削除を書いたあとで同じキーで入れ直した新しい行は、削除されません。

書き込みモード: テーブルが記憶する

SparkがDELETE・UPDATE・MERGEをどちらの方式で行うかは、テーブルプロパティのwrite.delete.mode・write.update.mode・write.merge.modeが決め、3つともデフォルトはcopy-on-writeです。merge-on-readはバージョン2以上でのみ使えます。

MERGE INTOは、WHEN MATCHED(削除・更新)とWHEN NOT MATCHED(挿入)を1回のコミットで行います。ソースの1行に変更が2つ対応すると、どちらを適用すべきかわからず失敗するので、変更のバッチはキーごとに1つに整理しておきます。

現場での姿

CDCのロード先テーブルがだんだん遅くなる場合です。数分おきにMERGEするmerge-on-readのテーブルは、削除ファイルがたまり続け、読むたびにそれを突き合わせます。定期的なコンパクション(rewrite_data_filesの削除の割合・個数の基準、rewrite_position_delete_files)で、削除をデータに畳み込む必要があります。

ほかのエンジンが削除した行を返す場合です。削除ファイルを理解できないエンジンは、merge-on-readのテーブルで削除した行まで読みます。複数のエンジンが同じテーブルを読むなら、各エンジンがどの削除形式をサポートしているかを、先に確認します。ラボ環境のDuckDB 1.5.5とpyiceberg 0.12は、バージョン2の位置削除を反映して読みます。

1日1回大きく変更するテーブルの場合です。夜間バッチが昨日のパーティションを丸ごと直し、昼間はダッシュボードが何百回も読むなら、copy-on-writeのほうが適しています。書き込みコストは1回で、読み取りの利得は何百回です。

実務で本当に大切なこと

次のラボですること

同じ3月の注文でcopy-on-writeのテーブルとmerge-on-readのテーブルを作り、同じDELETEを実行して、一方はデータファイルを書き直し、もう一方は位置削除ファイルだけを加えることを、コミットの要約で確認します。変更のバッチで2つのテーブルに同じようにMERGEしたあと、位置削除ファイルを1つpyarrowで直接開いて、どのファイルの何番目の行を指しているかを見て、2つのMERGEが書いたバイト数を比べ、DuckDBとpyicebergが削除を反映して読むかを確認します。