レイクハウスのテーブル形式 — Apache Iceberg をメタデータで理解する
同じ削除と MERGE を二通りで — ファイルを書き直すか、消す行を書き留めるか
目標
同じ3月の注文でcopy-on-write(COW)のテーブルとmerge-on-read(MOR)のテーブルを作り、同じDELETEと同じMERGEを2つのテーブルに実行します。COWは変更された行が入ったデータファイルを丸ごと書き直し、MORは「どのファイルの何番目の行を削除する」という位置削除ファイルだけを加えることを、スナップショットの要約とファイルで確認します。位置削除ファイルを直接開いて見て、ほかのエンジン(DuckDB・pyiceberg)がその削除を反映して読むかを確認します。
なぜ重要なのか
Parquetファイルは書き換えられません。1行を変えるには、そのファイルを新しく書き直すか(COW)、その行が削除されたという事実を別のファイルに書いておいて、読むときに取り除く(MOR)必要があります。COWは、読み取りが安く書き込みが高いです。100万行のファイルで1行を直しても、100万行を書き直します。MORは、書き込みが安く読み取りが高いです。読むたびに削除ファイルをデータファイルと突き合わせなければならず、削除ファイルがたまるほど遅くなります。
CDCのように小さな変更が頻繁に来るテーブルは、MORで書いて定期的にコンパクションするのが普通で、1日1回大きく変更して多く読まれるテーブルは、COWが適しています。どちらの場合も、テーブルプロパティ3つ(write.delete.mode・write.update.mode・write.merge.mode)で決まり、判断の根拠は感覚ではなく、コミットごとに要約に残る書き込みバイト数とファイル数です。
ステップ
- /root/ice/rl/tables.py(アプリ
ice-rl-tables)で、lake.rl.cow(3つのモードはすべてcopy-on-write)とlake.rl.mor(3つのモードはすべてmerge-on-read)をformat-version 2で作成し、3月の1か月分をそれぞれ1回で入れてください。 - /root/ice/rl/delete_cow.py(アプリ
ice-rl-delete-cow)で、DELETE FROM lake.rl.cow WHERE status = 'cancelled'を実行してください。 - /root/ice/rl/delete_mor.py(アプリ
ice-rl-delete-mor)で、同じ削除をlake.rl.morに実行してください。 - /root/ice/rl/merge.py(アプリ
ice-rl-merge)で、/data/ice/changes.csv(opがU・D・I)を2つのテーブルに同じようにMERGEしてください。 - /root/ice/rl/posdel.pyで、
lake.rl.morの位置削除ファイルを1つpyarrowで開き、/root/ice/rl/out/posdel.jsonに書いてください。 - /root/ice/rl/cost.pyで、2つのテーブルのMERGEコミットが書いたバイト数(
added-files-size)を、/root/ice/rl/out/cost.jsonに書いてください。 - /root/ice/rl/read.pyで、
lake.rl.morをDuckDBとpyicebergで読み、/root/ice/rl/out/read.jsonに書いてください。 - /root/ice/rl/report.mdに、
## 삭제 두 방식、## 위치 삭제 파일、## 쓰기와 읽기의 맞바꿈の3つの節を書いてください(3つの見出しは順に、韓国語で「削除の2方式」「位置削除ファイル」「書き込みと読み取りのトレードオフ」を意味する語句です)。
参考
- 変更バッチの列は
op, order_id, customer_id, region, amount, status, order_tsです。Uはstatusをrefundedに変え、Dは削除し、Iは新しい注文です。 - コミットの要約は
SELECT operation, summary FROM lake.rl.mor.snapshotsで、ファイルの種類はSELECT content, file_path, record_count FROM lake.rl.mor.files(content 0がデータ・1が位置削除・2が等価削除)で見ます。 - このラボのテーブルはformat-version 2なので、位置削除ファイルはParquetで書かれます。format-version 3では、同じ情報がdeletion vector(Puffin)で書かれます。
- よくある間違いは、ステップ2を2つのテーブルの両方に実行すること(ステップ3と順序が混ざると、比較がずれます)、MERGEを2回実行することです。元に戻すには、2つのテーブルを
DROP TABLE … PURGEしてから、ステップ1からやり直してください。 - 公式ドキュメント: Spark Writes — MERGE INTO・Configuration — write.delete.mode・Spec — Row-level Deletes・DuckDB — Iceberg extension
同じデータ、違う書き込みモード
/root/ice/rl/tables.pyをアプリ名ice-rl-tablesで作成し、lake.rl.cowとlake.rl.morを作ってください。どちらも列は6つ・'format-version' = '2'で、write.delete.mode・write.update.mode・write.merge.modeを、cowはすべてcopy-on-write、morはすべてmerge-on-readにします。3月の31ファイルを、各テーブルに1回ずつ入れてください。
書き込みモードはテーブルプロパティなので、エンジンではなくテーブルが記憶します。どのエンジンが書いても同じ方式に従わせるためです。採点ツールは、プロパティ6つと最初のコミットの行数を確認します。
COWの削除: ファイルを書き直す
/root/ice/rl/delete_cow.pyをアプリ名ice-rl-delete-cowで作成し、DELETE FROM lake.rl.cow WHERE status = 'cancelled'を実行してください。
キャンセルされた注文が入ったデータファイルは、キャンセルを除いた新しいファイルに置き換わります。要約のdeleted-data-filesとadded-data-filesがそれで、削除ファイルは1つもありません。採点ツールは、cowの2番目のコミットの要約と、そのときキャンセルされた注文が残っていないかを確認します。
MORの削除: 削除する行を書き留める
/root/ice/rl/delete_mor.pyをアプリ名ice-rl-delete-morで作成し、DELETE FROM lake.rl.mor WHERE status = 'cancelled'を実行してください。
データファイルはそのままにして、削除する行の(ファイルパス、行番号)を集めた位置削除ファイルを加えます。要約にadded-position-delete-filesができ、deleted-data-filesはありません。採点ツールは、morの2番目のコミットの要約と、マニフェストのcontentが1のファイルを確認します。
MERGE: 直し、消し、加える
/root/ice/rl/merge.pyをアプリ名ice-rl-mergeで作成し、/data/ice/changes.csvを一時ビューとして読み込んで、2つのテーブルのそれぞれにMERGE INTO … ON t.order_id = s.order_idで、opがDのものはDELETE、opがUのものはstatus・amountをUPDATE、テーブルにないopがIのものはINSERTしてください。
ソースの1行に変更が2つかかると、MERGEは失敗します(このバッチは、注文ごとに変更が1つだけです)。すでに削除されたキャンセル注文に対するU・Dは、対応する行がないので何もしません。採点ツールは、2つのテーブルの内容を、元のデータと変更バッチから計算した期待値と比較し、morにだけ位置削除ファイルがあるかを確認します。
位置削除ファイルを開いてみる
/root/ice/rl/posdel.pyで、lake.rl.morの現在のスナップショットから、contentが1のファイルを1つpyarrow.parquet.read_tableで開き、/root/ice/rl/out/posdel.jsonに{"delete_file": 경로, "rows": 행 수, "targets": [그 파일이 가리키는 데이터 파일 경로, …]}(プレースホルダーは順に、パス、行数、そのファイルが指すデータファイルのパスです)を書いてください。
位置削除ファイルは、file_path・posの2列のParquetです。1行が「このデータファイルのpos番目の行はない」という意味です。採点ツールは、皆さんが書いた行数と対象の一覧をファイルから読み直して比較し、対象が現在有効なデータファイルであるかを確認します。
MERGE 1回で書いたバイト数
/root/ice/rl/cost.pyで、2つのテーブルの現在のスナップショット(= MERGEコミット)の要約からadded-files-sizeを読み取り、/root/ice/rl/out/cost.jsonに{"cow_added_bytes": 정수, "mor_added_bytes": 정수}(プレースホルダーは整数です)の形で書いてください。
COWは、変更が届いたデータファイルをすべて書き直し、MORは、新しい行・変更された行と削除ファイルだけを書きます。同じ変更1,400件で書いたバイト数の差が、そのまま書き込み増幅です。採点ツールは、2つの値を要約と比較し、COWのほうが大きいかを確認します。
ほかのエンジンも削除を反映して読むか
/root/ice/rl/read.pyで、lake.rl.morの現在のmetadataパスをiceberg_scan()に渡し、DuckDBで行数とsum(amount)を、pyicebergで行数を数えて、/root/ice/rl/out/read.jsonに{"duckdb_rows", "duckdb_amount", "pyiceberg_rows"}の形で書いてください。
MORのテーブルは、読み取る側が削除ファイルを適用しないと、正しい結果になりません。削除ファイルを知らないエンジンは、削除した行まで返します。複数のエンジンが同じテーブルを読むなら、必ず確認することです。DuckDBのiceberg拡張は、ビルドのときにイメージに入れてあります(LOAD iceberg)。採点ツールは、3つの値を期待値と比較します。
どのテーブルをどのモードにするか
/root/ice/rl/report.mdに、## 삭제 두 방식、## 위치 삭제 파일、## 쓰기와 읽기의 맞바꿈の3つの節を書いてください(3つの見出しは順に、韓国語で「削除の2方式」「位置削除ファイル」「書き込みと読み取りのトレードオフ」を意味する語句です)。3つ目の節には、ステップ6の2つのバイト数の値を、数字で入れてください。
皆さんのチームのテーブルを2つ思い浮かべて、1つはCOW、1つはMORに決めるとしたら、何を根拠にしますか。MORを選ぶなら、削除ファイルがたまることを、いつ誰が片づけるかも書いてみてください。