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

データパイプライン

ファイルの置かれ方が読み取り費用を決める — パーティションとコンパクション

TT Labで続きを見る

目標

ファイルシステム上のパーティションレイクを扱うツールpq.pyを作ります。キーを変えながらファイル数とサイズの分布を測り、パーティションキーにかけた条件が、開くファイル数をどれだけ減らすかを測り、コンパクションを回し、その途中で読む側が何を見るかを確認し、キーを変えるコストを測ります。

なぜ重要なのか

同じ行を入れても、ファイルがどう置かれているかによって、クエリが開くファイル数が数十倍変わります。パーティションは、値ごとにディレクトリを分けておくことで、開かなかったファイルはコストが0です。ただし、その利点は、ディレクトリ名に入っている欄で絞るときにだけ生じます。だからといって、よく使う欄をすべてキーに入れると、パーティション数が値の積の分だけ増え、ファイル1つが数行になります。ファイルを開く固定コストが、内容を読むコストより大きくなり、一覧のエントリ数が爆発し、圧縮が効かなくなります。そのため、キーを選ぶことは、常にトレードオフです。小さなファイルが積み重なると、コンパクションを回します。難しいのは、コンパクションそのものではなく、その途中で読む人が何を見るかです。ディレクトリをたどってファイルを集める読み手は、古いファイルと新しいファイルの両方を拾って、同じ行を2回数えます。読む側がマニフェストを見るようにし、一覧の置換を最後に1回だけ行えば、その間はなくなります。Parquetは使いません。ラボのイメージにpyarrowがなく、Podはランタイムでのインストールができません。行グループのような概念は、読み物で公式ドキュメントとして扱いました。ここでは、パーティションディレクトリとマニフェストとJSON Linesで、同じ構造を手で作ります。採点ツールは、提出された文言を信じません。一時作業フォルダに、採点ツールが作った元データを用意して、自分で作ったツールを実際に動かし、マニフェストに書かれたサイズがディスク上の実際のサイズと合っているか、行が保存されているか、切り落としたファイル数が合っているかを見ます。元データと目標サイズは、実行ごとに変わります。

ステップ

  1. /root/parts/gen_orders.pyを作成して実行し、/root/parts/work/raw.jsonlを作ってください。
  2. /root/parts/pq.pyにwriteを作り、キー1つで分けたレイクとマニフェストを作らせてください。
  3. layoutを追加して、ファイル数とサイズの分布と、小さなファイルの個数を出力させてください。
  4. writeがキーを複数受け取り、1ファイルの行数を制限できるようにして、細かく分けたレイクを作ってください。
  5. queryを追加して、パーティションキーにかけた条件で、開くファイルを切り落とさせてください。
  6. compactを追加して、1つのパーティションの中の小さなファイルを、目標サイズに近づけてまとめさせてください。
  7. --crash=before-swapと--via=globを追加して、コンパクションの途中で読む側が何を見るかを確認してください。
  8. repartitionを追加して、キーを変えて書き直し、/root/parts/work/partition_report.mdにコストを書いてください。

参考

パーティションキーの候補が複数ある元データを作る

/root/parts/gen_orders.pyを作成して実行し、/root/parts/work/raw.jsonlを作ってください。200行以上で、1行はorder_id・day・region・channel・整数のamountを含み、dayは4種類以上、regionは3種類以上、channelは3種類以上である必要があります。

値の種類数が重要です。3つをすべてキーにしたときに、パーティションがいくつに増えるかが、このラボで見るものだからです。種類数を掛けて、元データの行数で割ってみると、ファイル1つに何行残るかが、あらかじめ見えます。シードを固定しておかなければ、キーを変えながら比べる間に、元データが揺れます。

キー1つで分けて一覧を残す

/root/parts/pq.pyにwrite <작업폴더> --lake=<이름> --key=<칸>(プレースホルダーは、作業フォルダ、名前、欄です)を作り、値ごとに칸=값(プレースホルダーは欄と値です)ディレクトリを作って、その下にpart-0000.jsonlを書き、レイクの中に_manifest.jsonを残させてください。

マニフェストのbytesは、ディスク上の実際のファイルサイズである必要があります。行の長さを足して計算した値を書くと、改行やエンコーディングのためにずれ、そのずれは、あとでコンパクションで、おかしな場所で区切ります。ファイルを書いたあとで、サイズをもう一度測ってください。マニフェストも、一時的な名前で書いてから付け替えれば、一覧が途中まで書かれた状態を読むことがなくなります。

ファイルがどんなサイズで置かれているかを測る

layout <작업폴더> --lake=<이름> --small=<바이트>(プレースホルダーは、作業フォルダ、名前、バイトです)を追加して、ファイル数・パーティション数・行数・全体のバイト数と、平均・中央値・最小・最大のサイズ、そして--small未満のファイル数を出力させてください。

平均は嘘をつきます。大きなファイル1つと、小さなファイル数百個が混ざると、平均は無事に見えます。中央値は最近傍順位でとり、補間しないでください。avg_bytesは、全体のバイト数をファイル数で割った商(切り捨て)です。これらの値は、すべてマニフェストから読んで計算します。

細かく分けて小さなファイルを作ってみる

writeが--key=day,region,channelのようにキーを複数受け取るようにし、--rows=<줄 수>(プレースホルダーは行数です)で、1ファイルの最大行数を制限できるようにしてください。そのうえで、細かく分けたレイクを作って、layoutで前のレイクと比べてください。

キーを1つ加えるたびに、パーティション数は、その欄の値の種類数だけ掛け算されます。ディレクトリは、キーの順に重ねて積んでください。day=2026-01-03/region=seoul/channel=app/です。--rowsがないか0なら、パーティションごとにファイル1つです。2つのレイクのsmall_filesを並べて見ると、何を失ったかが数字で見えます。

開かなかったファイルはコストが0

query <작업폴더> --lake=<이름> --where=<칸=값[,칸=값]>(プレースホルダーは、作業フォルダ、名前、欄と値です)を追加して、条件に合う行数と金額を出力しますが、パーティションキーにある欄でだけ、開くファイルを切り落とすようにしてください。応答には、files_totalと、実際に開いたfiles_scannedが、一緒に入ります。

キーにない欄は、ディレクトリ名に入っていないため、その条件では何も切り落とせません。そのとき、files_scannedは、files_totalと同じである必要があります。これを正直に数えることが、このステップのすべてです。ここで水増しすると、あとで何が遅いのかを、永遠に見つけられません。値は文字列として比べてください。

小さなファイルをまとめる

compact <작업폴더> --lake=<이름> --target=<바이트>(プレースホルダーは、作業フォルダ、名前、バイトです)を追加して、1つのパーティションの中のファイルをpathの順につなげ、次のファイルを足すと目標を超えるときに区切るようにしてください。新しいファイルをすべて書いたあとでマニフェストを付け替え、そのあとで古いファイルを消します。

順序が重要です。マニフェストを先に付け替えると、読む側が、まだないファイルを開きます。新しいファイルの名前は、古い名前と重なってはいけません。重なると、読んでいるファイルを上書きすることになり、そのパーティションが丸ごと空になります。世代番号を名前に入れれば、重なることはありません。行数は、コンパクションの前後で同じである必要があります。

コンパクションの途中で読む人が見るもの

compactに--crash=before-swapを追加して、新しいファイルを書き終えたあと、マニフェストを付け替える直前に、終了コード9で落ちるようにし、queryに--via=globを追加して、マニフェストを無視してディレクトリからpart-*.jsonlをたどる読み取りを作ってください。落としたあとで、2つの読み取りの答えを比べてください。

これが、このラボの核心の場面です。マニフェストで読む側は、何の変化も見ず、ディレクトリをたどる側は、同じ行を2回数えます。落ちたあとも、古いファイルと古いマニフェストは、そのままである必要があります。そのあと、コンパクションを正しく終えると、古いファイルが消え、2つの読み取りが、また同じになります。

キーを変える代償を見積もる

repartition <작업폴더> --from=<이름> --to=<이름> --key=<칸[,칸]>(プレースホルダーは、作業フォルダ、名前、欄です)を追加して、元データではなく、--fromレイクのマニフェストをたどってすべて読み、新しいキーで書き直すようにしてください。そのうえで、/root/parts/work/partition_report.mdに、## 무엇을 어떻게 쪼갰나、## 작은 파일 문제、## 묶기、## 파티션을 바꾸는 비용の4つの節で書いてください(韓国語の見出しで、順に「何をどう分けたか」「小さなファイル問題」「コンパクション」「パーティションを変えるコスト」を意味します)。

パーティションキーは、ディレクトリ構造そのものなので、変えることは、すべての行を書き直すことです。rows_readとrows_writtenが同じかどうかが最初の確認で、bytes_readとbytes_writtenが、その作業の実際の値です。レポートには、そのバイト数を数字で書いてください。次の会議で、キーを変えようという話が出たときに必要な、唯一の数字です。