クエリのパフォーマンスを理解する
一般的な考慮事項
- クエリのパースと分析
- クエリ最適化
- クエリパイプラインの実行
- 最終処理
データセット
遅いクエリを見つける
クエリログ
system.query_log テーブルに保存されます。
ClickHouse は、実行された各クエリについて、クエリの実行時間、読み取った行数、CPU、メモリ使用量、ファイルシステムキャッシュのヒット数などのリソース使用量といった統計情報を記録します。
そのため、低速なクエリを調査する際は、まずクエリログを確認するのがよいでしょう。実行に時間のかかっているクエリを簡単に特定でき、それぞれのリソース使用量も確認できます。
それでは、NYC taxi データセットで実行時間が長いクエリの上位 5 件を見つけてみましょう。
query_duration_ms は、そのクエリの実行にどれくらい時間がかかったかを示します。クエリログの結果を見ると、最初のクエリの実行に 2967ms かかっており、改善の余地があることがわかります。
また、メモリや CPU を最も多く消費しているクエリを調べることで、どのクエリがシステムに負荷をかけているのかを把握したい場合もあるでしょう。
enable_filesystem_cache 設定を 0 にしてファイルシステムキャッシュを無効にすることが重要です。
これらのクエリで何を実現しているのか、もう少し詳しく見てみましょう。
- クエリ 1 は、平均速度が時速 30 マイルを超える乗車について、距離の分布を計算します。
- クエリ 2 は、週ごとの乗車回数と平均コストを求めます。
- クエリ 3 は、データセット内の各移動の平均所要時間を計算します。
EXPLAINステートメント
nyc_taxi.trips_small_inferred テーブルからデータを読み取ります。次に、WHERE 句を適用して計算済みの値に基づいて行をフィルタリングします。フィルタリングされたデータは集計用に準備され、分位点が計算されます。最後に、結果がソートされて出力されます。
ここで、主キーが使用されていないことがわかります。これはテーブル作成時に主キーを定義しなかったため、当然のことです。その結果、ClickHouse はクエリに対してテーブルのフルスキャンを実行しています。
Explain Pipeline (実行計画パイプライン)
EXPLAIN Pipelineは、クエリの具体的な実行戦略を示します。ここでは、先ほど確認した汎用的なクエリプランをClickHouseが実際にどのように実行したかを見ることができます。
進め方
system.query_logs の user、tables、databases フィールドを使って検索対象を絞り込めます。
最適化したいクエリを特定したら、そのクエリの改善に着手できます。この段階で開発者がよく犯すミスの 1 つは、複数の変更を同時に加え、場当たり的な実験を行い、結果として評価が混在してしまうことです。さらに重要なのは、何がクエリを高速化したのかを十分に理解できなくなることです。
クエリ最適化には、体系立った進め方が必要です。高度なベンチマークの話ではありません。変更がクエリ性能にどう影響するかを理解するためのシンプルな手順を用意するだけでも、大きな効果があります。
まずクエリログから遅いクエリを特定し、その後、改善の可能性を個別に調査します。クエリをテストするときは、必ずファイルシステムキャッシュを無効にしてください。
ClickHouse は、クエリ性能を向上させるために、さまざまな段階でキャッシュを活用しています。これはクエリ性能には有益ですが、トラブルシューティング時には、潜在的な I/O ボトルネックや不適切なテーブルスキーマを見えにくくしてしまう可能性があります。そのため、テスト中はファイルシステムキャッシュをオフにすることをお勧めします。本番環境では有効にしておいてください。最適化の候補を特定したら、それぞれが性能にどう影響するかをより正確に把握できるよう、1 つずつ実装することをお勧めします。以下は一般的な進め方を示した図です。 最後に、外れ値には注意してください。ユーザーが場当たり的に高コストなクエリを試したり、別の理由でシステムに負荷がかかっていたりして、クエリの実行が遅くなることは珍しくありません。
normalized_query_hash フィールドでグループ化すると、継続的に実行されている高コストなクエリを特定できます。そうしたクエリこそ、優先的に調査すべき対象である可能性が高いでしょう。
基本的な最適化
Nullable
mta_tax と payment_type の 2 つだけです。残りのフィールドに Nullable カラムを使うべきではありません。
低カーディナリティ
ratecode_id、pickup_location_id、dropoff_location_id、vendor_id) は、LowCardinalityフィールド型に適した候補です。
データ型を最適化する
最適化を適用する
クエリ時間とメモリ使用量の両方で、一定の改善が見られます。データスキーマを最適化したことで、データの表現に必要な総データ量が減り、その結果、メモリ消費量の改善と処理時間の短縮につながっています。
違いを確認するため、テーブルのサイズを見てみましょう。
主キーの重要性
ClickHouse における granule は、クエリ実行時に読み取られるデータの最小単位です。granule には、index_granularity によって決まる固定数までの行が含まれ、デフォルト値は 8192 行です。granule は連続して格納され、主キー順にソートされます。パフォーマンスの観点から、適切な主キーの組み合わせを選ぶことは重要です。実際、特定のクエリ群を高速化するために、同じデータを異なる table に格納し、それぞれで異なる主キーの組み合わせを使うことは珍しくありません。 Projection や materialized view など、ClickHouse がサポートするほかの選択肢を使えば、同じデータに対して異なる主キーの組み合わせを利用できます。このブログシリーズの第 2 部では、これについてさらに詳しく説明します。
主キーを選ぶ
- ほとんどのクエリでフィルタに使うフィールドを選ぶ
- カーディナリティの低いカラムから先に選ぶ
- タイムスタンプを含むデータセットでは時間でフィルタすることが多いため、主キーに時間ベースの要素を含めることを検討する。
passenger_count、pickup_datetime、dropoff_datetime を主キーとして試します。
passenger_count のカーディナリティは小さく (一意な値は 24 個) 、低速なクエリでも使われています。また、タイムスタンプのフィールド (pickup_datetime と dropoff_datetime) も、頻繁にフィルタされるため追加します。
主キーを設定した新しいテーブルを作成し、データを再度取り込みます。
| クエリ 1 | |||
|---|---|---|---|
| 実行 1 | 実行 2 | 実行 3 | |
| 経過時間 | 1.699 sec | 1.353 sec | 0.765 sec |
| 処理行数 | 329.04 million | 329.04 million | 329.04 million |
| ピークメモリ | 440.24 MiB | 337.12 MiB | 444.19 MiB |
| クエリ 2 | |||
|---|---|---|---|
| 実行 1 | 実行 2 | 実行 3 | |
| 経過時間 | 1.419 sec | 1.171 sec | 0.248 sec |
| 処理行数 | 329.04 million | 329.04 million | 41.46 million |
| ピークメモリ | 546.75 MiB | 531.09 MiB | 173.50 MiB |
| クエリ 3 | |||
|---|---|---|---|
| 1回目 | 2回目 | 3回目 | |
| 経過時間 | 1.414 sec | 1.188 sec | 0.431 sec |
| 処理行数 | 329.04 million | 329.04 million | 276.99 million |
| ピークメモリ | 451.53 MiB | 265.05 MiB | 197.38 MiB |