最近、バケッティングに関するブログを書く機会があった。
ここで取り上げたパーティショニングとバケッティングはビッグデータ処理におけるIO削減手法の代表的な例ではあるが、両者の仕組み(特に後者)や使いどころの違いは案外知られていない気がするので、この機に整理しておく。
また、それぞれにやり方がいくつかあり、これも毎度ややこしいのだが、こちらについては項を改める。
前提
話を単純化するため、以下の前提で記載する。
- Amazon S3との組み合わせで使う。 S3以外のデータストアはいったん忘れる
- Amazon Athena、AWS Glueを用いる。 EMRとかEC2などはいったん忘れる
- HiveとSparkの範囲内で使う。 Icebergとかはいったん忘れる
1)基礎
両方ともIO量を削減するためのテクニックだが、アプローチが異なる。
パーティショニングのコンセプト
指定した列の値ごとにデータを物理に異なるS3プレフィックスに配置することで、クエリー時に読み取るS3プレフィックス数を減らせるようにデータを準備する手法。
クエリーの際、WHERE句でパーティション列と特定の値を指定し、その値以外のS3プレフィックスを読み飛ばすことでIOを削減する。
バケッティングのコンセプト
指定した列の値を元に配置先のオブジェクトを計算・配置することで、読み取るS3オブジェクト数を減らせるようにデータを準備する手法。
※このオブジェクトを「バケット」と呼ぶので大変ややこしいが、S3用語としてのバケットとは何の関係もない。
仕組みとしてはAmazon Kinesis Data Streamsのパーティションキーと似通っていて、ハッシュ計算によって格納先のバケットを決める。このため、同じ値のレコードは常に同じオブジェクト内に収まるし、クエリー時に同様の計算をすることで特定の値がどのオブジェクトに格納されているかも高速に算出される。
なお対象のS3オブジェクト(バケット)はS3プレフィックス(パーティション)ごとに計算される仕組み。つまりバケット数を仮に10とすれば、各S3プレフィックス内に10個ずつS3オブジェクトが作成される。
クエリーの際、WHERE句でバケット列と特定の値を指定し、その値が含まれないS3オブジェクトを読み飛ばすことでIOを削減する。
2)使いどころ
パーティショニングの使いどころ
年/月/日など、日付でパーティションを切るのが最も一般的で、たいがいの時系列データに有効なため広く使われている(year=2024/month=05
のようなプレフィックス構造をどこかで目にしたことがあると思う)。
都道府県など、日付以外の列で切ることもでき、これらは日付と階層構造にしても効果がある(例:city=Tokyo/year=2024/month=05/
)。
弱点として、対象の列のカーディナリティが高すぎるとパーティション数が多くなりすぎ、実行速度やコストの面で残念な結果になる。もう少し具体的に言うと、ETLジョブ実行時にS3プレフィックスの作成数が多すぎてPutのコストが高騰したり、パーティションの列挙に時間がかかったり(この弊害は近年解決されている)、パーティションあたりのデータ量が少なすぎてオブジェクトサイズが小さくなりクエリーが非効率になったりする。
また、当然だが、その列(例えば日付)がWHERE句に指定されないとIO削減効果は得られないので、頻用されるクエリーから逆算してパーティション列を決定することが重要となる。
バケッティングの使いどころ
カーディナリティが高すぎてパーティショニングには適さない列、例えばIDのような列に対して、クエリー時のIO量を減らしたい時に威力を発揮する。例えば、ID列に対してバケット数=20に設定すると、S3プレフィックスごとに20個のS3オブジェクトが作成されて、あるIDのデータは常に特定のS3オブジェクトに格納される。この時、クエリー時にWHERE id in [x,y]
とすれば、x
とy
のデータは理論上最大で2つ、ベストケースで1つのS3オブジェクトに格納されていることになるので、残りの18ファイル(または19ファイル)は読む必要がなくなる。
逆にカーディナリティが低すぎると、効果は限定的になる。例えば指定した列が含む値が二値の場合(例:True, False)、3つ以上のバケット=S3オブジェクトは作成されず、クエリー時に読み飛ばせるオブジェクト数は最大で全体の50%に留まることになる。
カーディナリティだけでなく、偏差も効果に影響する。x
、y
、その他の値が概ね同程度の数であればS3オブジェクトごとのデータ量は概ね均等に配置されるが、例えばx
が極端に多い場合(IDを例に取ると、特定のユーザーだけ突出して利用回数が多く時系列データ量が多いなどのケースが該当する)、x
の含まれるファイルだけが巨大になるといった不均衡が生まれる。
つまり、バケッティングの効果を高めるためには、値のカーディナリティが十分に高く、偏差がなるべく少ない列を選ぶことが肝要と言える。
あくまで一般論であって、バケッティングの効果はデータやクエリーによってかなりばらつく傾向があるので、実際の効果は測定してみることをお勧めしたい。
なお、WHERE句で指定しなければ無用の長物という点はパーティショニングと同様なので、「どういうクエリーを使いたいか」が最重要なのはここでも同じである。
まとめ
パーティショニングは比較的普遍的に利用できる。特に、時系列データでは多くの場合日付ベースでのクエリーが想定されることから、とりあえず日付でパーティションを切っておけばある程度効果が見込める利便性があり、広く利用されている。
一方、バケッティングはユースケースが限られるせいか、パーティショニングに比べるとマイナーで情報も少ないが、ハマればちゃんとIO量を減らせる。ただし列の選定やバケット数などの設計値、およびデータの内容によって効果が変動する気難しさがあり、検証と設計がより重要になる印象。
いずれも物理的な配置を伴う(厳密にはIcebergでは異なるがここでは割愛)ため、洗い替えを避けるには頻用クエリーを起点とした当初の設計が重要となる。
本記事がその一助となれば幸いです。