パーティションキーが以後のすべてのクエリを決める
一言でいうと
パーティションキーを何にするかが、今後のすべてのクエリのコストを決め、細かく分けるほど小さなファイルが積み重なり、誤って選んだときに戻す代償は、全体を書き直すコストです。
なぜ必要なのか
同じ行を入れたのに、あるクエリは0.2秒で終わり、あるクエリは40秒かかります。クエリは同じで、データも同じです。違うのは、ファイルがディスク上にどう置かれているかの1点だけです。
パーティションは、値ごとにディレクトリを分けておくことです。day=2026-01-03/の下に、その日のデータだけがあれば、その日だけを見るクエリは、残りのディレクトリをそもそも開きません。開かなかったファイルはコストが0です。これが、パーティションがもたらす、ほぼ唯一の、そして非常に大きな利点です。
問題は、その利点がキーにかけた条件にだけ生じることです。ディレクトリ名にない欄で絞り込むと、切り落とせるものがなく、すべて開いてみる必要があります。そのため、キーを選ぶことは、データの性質ではなく、今後入ってくるクエリの形を選ぶことです。
どう動くのか
「それなら、よく使う欄をすべてキーに入れればいい」が、最初の落とし穴です。日付と地域と流入経路をすべてキーにすると、パーティション数は3つの値の積になります。1日300件のデータが、突然160個のディレクトリに散らばり、ファイル1つが2、3行になります。
これが小さなファイル問題です。損失は3か所で生じます。ファイル1つを開くための固定コストが、内容を読むコストより大きくなり、一覧を管理する側(マニフェストでもメタストアでも)のエントリ数が爆発し、圧縮とエンコーディングがうまく効きません。
ファイル1つが、読み取りの単位とどうかみ合うかを見ると、なぜ小さなファイルが損なのかがはっきりします。Parquetのコンセプトのドキュメントは、行グループ(row group)を「データを行方向に論理的に分けたもの」と定義し、カラムチャンク(column chunk)を「特定の行グループの中にあり、ファイルの中で連続していることが保証された、1つのカラムのデータ」と定義しています。連続しているという言葉が核心です。1回の大きな順次読み取りで取得できるという意味です。ページ(page)は、その下の単位で、「圧縮とエンコーディングの観点で、それ以上分けられない単位」です。
Parquetの設定ドキュメントは、行グループを512MBから1GBと大きくとることを勧めています。大きな順次入出力と大きなカラムチャンクを得るためで、行グループ1つがブロック1つに収まるように合わせることを、理想的な配置として挙げています。ページは8KBを勧めていますが、小さいほど1行だけを探して読むことが容易になるからです。
ここから結論が出ます。ファイルが行グループよりはるかに小さいと、その構造は何の働きもできません。2KBのファイルに、512MBの行グループを入れることはできません。読む側は、ファイルごとに末尾のメタデータを読んで開くことを繰り返すだけです。そのため、ファイルサイズは好みではなく、読み取りの単位とのかみ合わせです。
このラボは、Parquetを使いません。ラボのイメージにpyarrowがなく、Podはランタイムでのインストールができません。そのため、パーティションディレクトリとマニフェストとJSON Linesで、同じ構造を手で作ります。行グループという名前がないだけで、ファイル1つが読み取りの単位とかみ合うという話は、そのままです。
コンパクションと、その途中の読み手
小さなファイルが積み重なると、コンパクション(compaction)を回します。1つのパーティションの中の小さなファイルを、目標サイズに近づけてつなげ、大きなファイルに変えることです。難しくはありません。難しいのは、コンパクションの途中で読む人が何を見るかです。
新しいファイルを書いている間、古いファイルもそのままあります。このとき、ディレクトリをたどってファイルを集める読み手は、古いファイルと新しいファイルの両方を拾って、同じ行を2回数えます。合計がちょうど2倍になる事故が、ここで起きます。
防ぐ方法は1つです。読む側が、ディレクトリではなく一覧(マニフェスト)を見ます。コンパクションは、新しいファイルをすべて書いたあとで、一覧を一度に付け替え、そのあとで初めて古いファイルを消します。一覧の置換がアトミックなら、読み手は、古い一覧全体か新しい一覧全体を見ます。その間はありません。
新しいファイルの名前にも、ルールがあります。古い名前と重なってはいけません。重なると、読んでいるファイルを上書きすることになり、そのパーティションが丸ごと空になります。
現場での姿
1つ目は、キーを変える代償は、全体を書き直すことだということです。パーティションキーは、ディレクトリ構造そのものなので、変えるにはすべての行を読んで、すべてのファイルを新しく書かなければなりません。そのため、キーを選ぶ会議は、もう一度行う価値があります。
2つ目は、時間のキーは、ほとんどの場合入るということです。クエリの大半が期間で入ってきて、古いデータを丸ごと消すことも、ディレクトリ単位になるからです。
3つ目は、値の多い欄は、キーに使わないということです。ユーザーIDや注文番号をキーにすると、パーティションが行数と同じだけできます。このミスは、データが少ないときは何の症状もなく、数か月後に明らかになります。
4つ目は、平均のファイルサイズは嘘をつくということです。大きなファイル1つと、小さなファイル数百個が混ざると、平均は無事に見えます。中央値としきい値未満のファイルの個数を、合わせて見て初めて、実際の形が見えます。
実務で本当に大切なこと
- キーは、クエリの形から選びます。データの欄の一覧から選びません。
- キーを加えるたびに、パーティション数は掛け算されます。3つなら、掛け算が3回です。
- 平均ではなく、中央値と小さなファイルの個数を見ます。
- 読む側は、一覧を見ます。ディレクトリをたどる読み手は、コンパクションの途中で2倍を見ます。
- コンパクションのとき、新しい名前は古い名前と重ならないようにします。
次のラボですること
注文の元データを作り、ツールpq.pyを1ステップずつ育てます。日付1つで分けたレイクと、日付・地域・経路で細かく分けたレイクを、それぞれ作って、ファイル数とサイズの分布を比べ、パーティションキーにかけた条件と、かけていない条件で、開くファイル数がどう変わるかを測ります。そのあと、コンパクションを回し、その途中で落として、マニフェストで読む側と、ディレクトリをたどる側が、それぞれ何を見るかを確認します。最後に、キーを変えて書き直し、そのコストを書きます。採点ツールは、毎回異なる元データと目標サイズで、自分で作ったツールを実際に動かして、答えを突き合わせます。