メインコンテンツまでスキップ
メインコンテンツまでスキップ

PREWHERE最適化はどのように機能しますか?

PREWHERE句は、ClickHouseにおけるクエリ実行の最適化手法です。これによりI/Oが削減され、不要なデータの読み取りを避け、非フィルタカラムをディスクから読み込む前に無関係なデータがフィルタリングされることで、クエリ速度が向上します。

このガイドでは、PREWHEREがどのように機能するのか、その影響を測定する方法、そして最適なパフォーマンスを得るための調整方法について説明します。

PREWHERE最適化なしのクエリ処理

まず、uk_price_paid_simpleテーブルに対するクエリがPREWHEREを使用せずに処理される方法を示します:



① クエリには、テーブルの主キーの一部であるtownカラムに対するフィルタが含まれており、したがって主インデックスの一部でもあります。

② クエリを加速するために、ClickHouseはテーブルの主インデックスをメモリに読み込みます。

③ インデックスエントリをスキャンし、townカラムからどのグラニュールが述語に一致する行を含む可能性があるかを特定します。

④ これらの潜在的に関連するグラニュールは、クエリに必要な他のカラムからの位置が揃ったグラニュールと共にメモリに読み込まれます。

⑤ 残りのフィルタは、クエリ実行中に適用されます。

ご覧の通り、PREWHEREがない場合、実際に一致する行が少ない場合でも、すべての潜在的に関連するカラムがフィルタリングされる前に読み込まれます。

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は① 選択された2つのグラニュールをpriceカラムから読み取り、最後のフィルタprice > 10_000を評価します。

2つのグラニュールのうち1つのみが一致する行を含んでいるため、② その位置が揃ったグラニュールのみがSELECTカラムであるstreetのために読み込まれます:



最終ステップでは、一致する行を含む最小限のカラムグラニュールのセットのみが読み込まれます。これにより、メモリ使用量が低下し、ディスクI/Oが削減され、クエリ実行が速くなります。

PREWHEREは読み取るデータを削減し、処理する行は削減しない

ClickHouseはPREWHEREバージョンでも非PREWHEREバージョンでも、同じ数の行を処理します。ただし、PREWHERE最適化が適用されている場合、処理された各行のすべてのカラム値を読み込む必要はありません。

PREWHERE最適化は自動的に適用される

PREWHERE句は手動で追加することができますが、上記の例のようにPREWHEREを手動で書く必要はありません。optimize_move_to_prewhere設定が有効になっている場合(デフォルトでtrue)、ClickHouseは自動的にWHEREからPREWHEREにフィルタ条件を移動し、読み取りボリュームを最も削減できる条件を優先します。

小さいカラムはスキャンが速いため、より大きなカラムが処理されるまでに、ほとんどのグラニュールがすでにフィルタリングされているという考え方です。すべてのカラムに同じ数の行があるため、カラムのサイズは主にそのデータ型によって決まります。たとえば、UInt8カラムは通常Stringカラムよりもはるかに小さくなります。

ClickHouseはバージョン23.2からこの戦略をデフォルトで採用しており、PREWHEREフィルタカラムを未圧縮サイズの昇順でマルチステップ処理のためにソートします。

バージョン23.11以降、オプションのカラム統計を使用することで、カラムサイズだけでなく、実際のデータの選択性に基づいてフィルタ処理の順序を選択することができ、さらに改善されます。

PREWHEREの影響を測定する方法

PREWHEREがクエリに役立っていることを確認するために、optimize_move_to_prewhere設定が有効な場合と無効な場合のクエリ性能を比較することができます。

まず、optimize_move_to_prewhere設定が無効の状態でクエリを実行します:

ClickHouseはクエリの処理中に23.36 MBのカラムデータを読み込みました。

次に、optimize_move_to_prewhere設定が有効な状態でクエリを実行します。(この設定はオプションですが、デフォルトでは有効です):

処理された行数は同じ (2.31百万) ですが、PREWHEREのおかげでClickHouseはカラムデータを3倍以上少なく読み込みました—23.36 MBの代わりにわずか6.74 MBであり、全体の実行時間を3分の1に短縮しました。

ClickHouseがPREWHEREをどのように適用しているかをより深く理解するために、EXPLAINとトレースログを使用します。

EXPLAIN句を使用してクエリの論理プランを調べます:

ここではプランの出力の大部分を省略していますが、それはかなり冗長です。要するに、すべての3つのカラム述語が自動的にPREWHEREに移動されたことを示しています。

これを自分で再現すると、クエリプランの中でこれらの述語の順序がカラムのデータ型サイズに基づいていることもわかります。カラム統計が有効になっていないため、ClickHouseはサイズをPREWHERE処理の順序を決定するためのフォールバックとして使用しています。

さらに深く掘り下げたい場合は、クエリ実行中にすべてのテストレベルのログエントリを返すようにClickHouseに指示することで、各PREWHERE処理ステップを観察できます:

重要なポイント

  • PREWHEREは後でフィルタリングされるカラムデータの読み取りを回避し、I/Oとメモリを節約します。
  • optimize_move_to_prewhereが有効な場合(デフォルト)には自動的に機能します。
  • フィルタリングの順序は重要です:小さく選択的なカラムを最初に配置すべきです。
  • EXPLAINやログを使用してPREWHEREが適用されていることを確認し、その効果を理解することができます。
  • PREWHEREは、幅広いテーブルや選択的フィルタによる大規模なスキャンに最も影響を与えます。