PREWHERE 最適化なしのクエリ処理
① このクエリには
town カラムに対するフィルタが含まれており、このカラムはテーブルの主キーの一部であるため、プライマリインデックスにも含まれます。
② クエリを高速化するため、ClickHouse はテーブルのプライマリインデックスをメモリに読み込みます。
③ インデックスエントリを走査し、town カラム内のどのグラニュールに条件に一致する行が含まれている可能性があるかを特定します。
④ こうして候補となったグラニュールが、クエリに必要な他のカラムの対応するグラニュールとあわせてメモリに読み込まれます。
⑤ その後、残りのフィルタがクエリの実行時に適用されます。
このように、PREWHERE を使わない場合は、実際に一致する行がわずかであっても、フィルタリングの前に関連する可能性があるすべてのカラムが読み込まれます。
PREWHERE がクエリ効率を向上させる仕組み
① クエリには
town カラムに対するフィルタが含まれており、これはテーブルの主キーの一部であるため、プライマリインデックスの一部でもあります。
② PREWHERE 句がない場合の実行と同様に、クエリを高速化するため、ClickHouse はプライマリインデックスをメモリに読み込みます。
③ その後、索引エントリを走査して、town カラムのどのグラニュールに述語に一致する行が含まれている可能性があるかを特定します。
ここからは、PREWHERE 句のおかげで次のステップが変わります。関連するすべてのカラムを最初にまとめて読み込むのではなく、ClickHouse はカラムごとにデータを絞り込み、本当に必要なものだけを読み込みます。これにより、特に列数の多いテーブルでは、I/O が大幅に削減されます。
各ステップでは、前のフィルタを通過した、つまり一致した行を少なくとも 1 行含むグラニュールだけを読み込みます。その結果、各フィルタで読み込みと評価の対象となるグラニュール数は段階的に減っていきます。
ステップ 1: town によるフィルタリングClickHouse は PREWHERE 処理を開始し、①
town カラムから選択されたグラニュールを読み取り、実際に London に一致する行が含まれているものを確認します。
この例では、選択されたすべてのグラニュールが一致するため、② 次のフィルタカラム date について、対応する位置のグラニュールが処理対象として選択されます。
ステップ 2: date によるフィルタリング
次に、ClickHouse は ① 選択された
date カラムのグラニュールを読み取り、フィルタ date > '2024-12-31' を評価します。
この場合、3 つのグラニュールのうち 2 つに一致する行が含まれているため、② 次のフィルタカラム price については、対応する位置のグラニュールだけが後続処理の対象として選択されます。
ステップ 3: price によるフィルタリング
最後に、ClickHouse は ①
price カラムから選択された 2 つのグラニュールを読み取り、最後のフィルタ price > 10_000 を評価します。
2 つのグラニュールのうち、一致する行を含むのは 1 つだけなので、② SELECT カラム street については、対応する位置のグラニュールだけを後続処理のために読み込めば十分です。
最終ステップでは、一致する行を含む最小限のカラムグラニュールだけが読み込まれます。これにより、メモリ使用量が減り、ディスク I/O が少なくなり、クエリ実行が高速化されます。
PREWHERE は読み取るデータ量を減らすのであって、処理する行数を減らすわけではありませんPREWHERE を使用する場合も使用しない場合も、ClickHouse が処理する行数は同じである点に注意してください。ただし、PREWHERE 最適化を適用すると、処理対象のすべての行について、すべてのカラム値を読み込む必要はなくなります。
PREWHERE 最適化は自動的に適用されます
optimize_move_to_prewhere が有効な場合 (デフォルトでは true) 、ClickHouse はフィルタ条件を WHERE から PREWHERE に自動的に移動し、その際、読み取り量を最も削減できるものが優先されます。
これは、小さいカラムほどスキャンが速く、大きいカラムを処理する段階では、ほとんどのグラニュールがすでに除外されているためです。すべてのカラムは同じ行数を持つため、カラムのサイズは主にデータ型によって決まります。たとえば、UInt8 カラムは通常 String カラムよりかなり小さくなります。
ClickHouse は、バージョン 23.2 以降、デフォルトでこの戦略を採用しており、複数段階の PREWHERE 処理では、フィルタ対象のカラムを非圧縮サイズの昇順でソートします。
バージョン 23.11 以降では、オプションのカラム STATISTICS を使うことで、単にカラムサイズではなく実際のデータの選択性に基づいてフィルタ処理の順序を決定でき、さらに改善できます。
PREWHERE の影響を測定する方法
optimize_move_to_prewhere setting を有効にした場合と無効にした場合のクエリのパフォーマンスを比較します。
まず、optimize_move_to_prewhere setting を無効にしてクエリを実行します。
optimize_move_to_prewhere 設定を有効にしてクエリを実行します (この設定はデフォルトで有効になっているため、省略可能です) :
要点
- PREWHERE は、後で除外されるカラムデータの読み取りを避けることで、I/O とメモリを節約します。
optimize_move_to_prewhereが有効 (デフォルト) であれば、自動的に適用されます。- フィルタリングの順序は重要です。サイズが小さく、選択性の高いカラムを先に置くべきです。
EXPLAINとログを使って、PREWHERE が適用されているかを確認し、その効果を把握します。- PREWHERE は、列数の多いテーブルや、選択性の高いフィルタを伴う大規模スキャンで特に効果を発揮します。