遅延マテリアライゼーション
この記事では、遅延マテリアライゼーションの仕組みと、ClickHouse の I/O 最適化スタック全体の中での位置付けについて説明します。 また、遅延マテリアライゼーションによってクエリパフォーマンスがどのように向上するかを示す実例も紹介します。
遅延マテリアライゼーションは ClickHouse のバージョン 25.4 で導入され、デフォルトで有効になっています。
Overview
長年にわたり、ClickHouse は I/O を積極的に削減するため、一連の階層的な最適化を導入してきました。 これらの手法は、その高速性と効率性を支える基盤となっています。
| Optimization | Description |
|---|---|
| Columnar storage | クエリに不要なカラム全体をスキップできるほか、類似した値をまとめることで高い圧縮率を実現し、データ読み込み時の I/O を最小化します。 |
| Sparse primary indexes | secondary data-skipping indexes | projections | インデックス付きカラム に対するフィルタと一致する可能性がある granules(行ブロック)を特定し、不要なデータを刈り込みます。これらの手法は granule レベルで動作し、個別にも組み合わせても利用できます。 |
| PREWHERE | インデックスがない カラムに対するフィルタについてもマッチを確認し、本来であれば読み込まれてから破棄されるデータを早期にスキップします。索引によって選択された granule をさらに絞り込む形でも、独立しても動作し、すべてのカラムフィルタに一致しない行をスキップすることで granule の刈り込みを補完します。 |
| Query condition cache | どの granule が前回すべてのフィルタにマッチしたかを記憶することで、繰り返し実行されるクエリを高速化します。これにより、たとえクエリの形が変わっても、マッチしなかった granule の読み取りとフィルタリングをスキップできます。 |
上記の I/O 最適化は読み取るデータ量を大きく削減できますが、それでもなお、WHERE 句を通過した行については、ソートや集約、LIMIT のような操作を実行する前に、すべてのカラムを読み込むことを前提としています。
しかし、一部のカラムはもっと後になるまで不要な場合や、WHERE 句を通過したにもかかわらず、結局まったく参照されないデータも存在し得ます。
そこで登場するのが lazy materialization です。これは I/O 最適化スタックを完成させる、直交する拡張機能です。
- 索引と
PREWHEREを組み合わせることで、WHERE句のカラムフィルタに一致する行だけが処理されるようになります。 - Lazy materialization はこれをさらに発展させ、実際にクエリ実行プランで必要になるまでカラムの読み取りを遅延させます。
フィルタリング後であっても、ソートのような次の処理に必要なカラムだけが即座に読み込まれます。
その他のカラムは後回しにされ、
LIMITの影響により、多くの場合は最終結果を生成するのに十分な一部だけが読み込まれます。 これにより、lazy materialization は Top N クエリに対して特に強力であり、最終的な結果には、しばしば巨大なカラムからごく少数の行だけがあれば足りるようになります。
実例での説明
レイジー・マテリアライゼーションについて詳しく知るには、ブログ記事 "ClickHouse gets lazier (and faster): Introducing lazy materialization" を強く推奨します。以下の例は前述のブログ記事から引用したもので、レイジー・マテリアライゼーションにより、ある ClickHouse クエリが 219 秒からわずか 139 ミリ秒(1576 倍の高速化)まで短縮できる様子を示しています。
インデックスと PREWHERE の恩恵を受けるには、クエリにはフィルターが必要です。インデックスには主キーのカラムに対するフィルターが、PREWHERE には任意のカラムに対するフィルターが必要になります。
レイジー・マテリアライゼーションはその上にきれいに積み重ねられますが、前述の他の最適化と異なり、カラムフィルターがまったく無いクエリでも高速化することができます。
次の例のクエリを考えてみます。このクエリは、日付、商品、評価、検証ステータスに関係なく、Amazon レビューのうち「役に立った」投票数が最も多いものを探し、そのタイトル、見出し、本文とともに上位 3 件を返します。
まずは、レイジー・マテリアライゼーションを無効にして(query_plan_optimize_lazy_materialization を使用し)、クエリを実行します(ファイルシステムキャッシュはコールドの状態):
次に、(今回もファイルシステムキャッシュをコールドな状態にして)クエリを再実行しますが、今度はレイジーマテリアライゼーションを有効にします:
通常は、レイジーマテリアライゼーションの利点を得るために query_plan_optimize_lazy_materialization = true を明示的に設定する必要はありません。
これはデフォルトで有効になっています。
遅延マテリアライゼーションを無効にした場合と有効にした場合とで、パフォーマンスがどのように変わるかを見てみましょう。
| メトリック | Lazy materialization オフ | Lazy materialization オン | 改善度 |
|---|---|---|---|
| 経過時間 | 219.071 秒 | 0.139 秒 | 約1576×高速 |
| 読み取りデータ量 | 71.38 GB | 1.81 GB | 約40×少ない |
| ピークメモリ使用量 | 1.11 GiB | 3.80 MiB | 約300×少ない |
クエリ実行プランでレイジーマテリアライゼーションを確認する方法
EXPLAIN 句を使用してクエリの論理実行プランを確認すると、先ほどのクエリでレイジーマテリアライゼーションが利用されていることを確認できます。
オペレータープランは下から上へ読み進めることができ、ClickHouse がソートと LIMIT の処理が終わるまで、大きな 3 つの String 型カラムの読み出しを遅延させていることが確認できます。