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

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

パーティションは列ではなく規則だ — 隠れたパーティショニングとパーティション進化

TT Labで続きを見る

一言でいうと

Icebergは、パーティション値を人が作った列ではなく、ソース列にかける変換ルール(例: days(order_ts))で計算してマニフェストに記録し、読み取る側はソース列の条件だけでファイルをスキップします。ルールはパーティション仕様IDを付けて積み重なるので、途中で変えても、古いファイルは古いルールのまま読めます。

なぜHive方式のパーティションが問題だったのか

Partitioningドキュメントが挙げる例は、典型的です。ログテーブルを日付で分けるには、Hiveではevent_dateという列を別に作り、書き込む側がevent_timeからその値を計算して入れる必要があります。ここから問題が次々に生まれます。

どう動くのか: 変換とパーティション仕様

Icebergのパーティション仕様は、フィールドごとにソース列ID、パーティションフィールドID、変換、名前を持ちます。エンジンは行を書き込むときに変換を計算し、そのファイルのパーティション値としてマニフェストに記録します。1つのファイルの行は、すべて同じパーティション値を持ちます。変換の一覧は次のとおりです。

変換 結果 用途
identity 値そのまま 種類の少ないカテゴリ列
year・month・day・hour 1970年から数えた年・月・日・時間 時刻の列
bucket[N] ハッシュをNで割った余り 種類の多いキー(顧客番号)
truncate[W] 幅Wで切り詰めた値 数値の区間、文字列の先頭部分
void 常にnull バージョン1でフィールドをなくすとき

bucketは、32ビットのMurmur3(x86、シード0)ハッシュの符号ビットを捨てて、Nで割ります。仕様がハッシュを定めているので、Sparkが書いてPythonが読んでも、同じ値は同じバケットに入ります。

読み取る側は、order_ts >= Xのようなソース列の条件だけを使います。スキャン計画が、その条件をパーティション条件(order_ts_day >= day(X))に変換して、マニフェストのパーティション値と比較します。この変換は「包含」(inclusive)する側に計算されるので、条件に合う行がありうるファイルは、決して漏れません。ユーザーは、パーティションがどう分かれているかを知らなくても構いません。だから「隠し」パーティショニングなのです。

パーティション進化: 古いファイルはそのまま

データが増えて、月単位では粗すぎるようになったとします。Evolutionドキュメントによると、仕様を変えても古いデータは古い仕様のまま残り、新しいデータだけが新しいレイアウトで書かれます。仕様はリストに積み重なり、マニフェストごとに自分が書いた仕様IDを記憶します。計画するときは、仕様ごとに別々に条件を変換してファイルを選びますが、ドキュメントはこれをsplit planningと呼びます。そして、パーティション進化はmetadataの操作なので、ファイルを急いで書き直すことはないと、はっきり記しています。

Sparkでは、ALTER TABLEでフィールドを追加し(ADD PARTITION FIELD)、削除し(DROP PARTITION FIELD)、置き換えます(REPLACE PARTITION FIELD … WITH …)。

ALTER TABLE lake.demo.events REPLACE PARTITION FIELD months(event_ts) WITH days(event_ts);

現場での姿

パーティションを変えたから古いデータも書き直さなければならないというのは、最もよくある誤解です。古い月単位のファイルは、そのままにしても正確に読めます。書き直すのは選択であり、理由が必要です。たとえば、古い期間を日単位で頻繁に照会して、読み取る量を減らしたいときのようにです。そのときも、コンパクション(rewrite_data_files)のような別の処理で行います。

1日の条件なのに1か月分を読む場合です。月単位のファイルに1日の条件をかけると、パーティションでは、その月のファイルを丸ごと選ぶことになります。ファイルの中の列統計(下限・上限)が役に立ちますが、ファイルが1か月分を均等に含んでいる場合は役に立ちません。計画が開くファイルの行数と、実際に該当する行数の差が、そのコストです。

顧客番号で分けたらパーティションが数万個になった場合です。identityで種類の多い列を分けると、小さなファイルが爆発します。bucketでバケット数を決めておくのが正解です。

実務で本当に大切なこと

次のラボですること

3月の1か月分をmonths(order_ts)で分けたテーブルに入れ、pyicebergで1日の条件の計画を立てて、何ファイル・何行を開くかを測ります。パーティションをdays(order_ts)に変えてから4月を入れ、3月のファイルは仕様0のまま、4月のファイルだけが仕様1であることを確認します。3月の1日と4月の1日の条件が読む行数を比べ、顧客テーブルをbucket(4, customer_id)で分けて、バケットが仕様どおりに計算されたかを確認します。