はじめに
以前、WHERE句でCASTをかけたことでパーティション/クラスタリングのプルーニングが効かなくなり、スキャン量が桁違いに膨らんだ話を書きました。今回はその前提となる、パーティションプルーニングとクラスタリングによるブロックプルーニングの基本的な仕組みを整理します。
パーティショニングとプルーニング
パーティショニングは、テーブルを日付や整数範囲などのキーに基づいて物理的にセグメント(パーティション)分割する仕組みです。パーティション自体は「データの分割」であり、それだけでスキャン量が減るわけではありません。
実際にスキャン量を減らすのは、クエリオプティマイザが「どのパーティションを読めばよいか」を判断し、不要なパーティションの読み込みを省略するプルーニングという処理です。パーティショニングは物理的な構造、プルーニングはその構造を活かすかどうかのロジック、と分けて理解すると分かりやすくなります。
プルーニングが効く条件
プルーニングが効くための基本条件は、WHERE句のパーティション列に対する条件が、BigQueryが解釈できる「定数的な」条件になっていることです。
-- プルーニングが効く例
SELECT * FROM dataset.table
WHERE transaction_date >= '2016-01-01'
逆に、以下のようなケースではプルーニングが効かず、テーブル全体(あるいは該当パーティション以外も含む広い範囲)がスキャンされます。
- パーティション列に対してサブクエリの結果や、別テーブルの値を条件にしている(定数ではなく動的な値のため)
- パーティション列とは別のカラム(
ts2など)を使った計算を条件にしている - パーティション列の型と、比較対象の値の型が一致していない(型変換が必要になる)
型の不一致については、例えば日付型のパーティション列に対してタイムスタンプ型の値で比較しようとすると、BigQueryは比較のために型変換を行う必要があり、その時点でプルーニングの対象外になってしまいます。
クラスタリングとブロックプルーニング
クラスタリングは、指定したカラムの値に基づいてテーブルのストレージブロックを並び替え、グルーピングする仕組みです。パーティショニングが「テーブル全体を大きな単位で分割する」のに対し、クラスタリングはパーティションの中(あるいはテーブル全体)をより細かいブロック単位で絞り込むためのものです。
クラスタリング対象のカラムに対してフィルタをかけると、BigQueryはそのカラムの値を持たないブロックを読み飛ばします。これをブロックプルーニングと呼びます。パーティショニングとクラスタリングを組み合わせることで、「まず日付でパーティションを絞り込み、さらにその中を特定のキーでブロック単位まで絞り込む」という2段階の絞り込みが可能になります。
クラスタリングを効かせるには、パーティションプルーニングと同様に、クラスタリング対象のカラムに対する条件が素直な形になっている必要があります。ここに関数や型変換を挟んでしまうと、オプティマイザはどのブロックに絞り込めるかを判断できなくなり、多くのブロックを読みに行かざるを得なくなります。
実行前にスキャン量を確認する
パーティションプルーニング・ブロックプルーニングが効いているかどうかは、クエリを実行する前にdry run(見積もり実行) で確認できます。BigQuery Consoleでクエリを入力すると、実行前に処理予定のバイト数が表示されるほか、bq query --dry_run コマンドでも同様の見積もりを取得できます。
想定より大きい見積もりバイト数が出た場合は、WHERE句の書き方(型の一致、関数の有無、動的な値を使っていないか)を見直す価値があります。
まとめ
パーティショニング・クラスタリングによるスキャン削減の仕組みを整理すると、次のようになります。
- パーティショニングは物理的なデータ分割であり、実際にスキャンを減らすのは「プルーニング」というオプティマイザの判断
- プルーニングが効くには、対象カラムへの条件が型・形式ともに素直な(定数的な)ものである必要がある
- クラスタリングはパーティションよりさらに細かい単位(ブロック)でのプルーニングを可能にし、パーティショニングと組み合わせることで2段階の絞り込みができる
- 型変換や関数を条件式に挟むと、いずれのプルーニングも効かなくなり、想定外のフルスキャンにつながる
「パーティション/クラスタリングを設定したから安心」ではなく、「クエリの書き方次第でその恩恵を受けられるかどうかが決まる」という点を意識しておくことが、コストとパフォーマンスの両面で重要になります。