SELECT クエリの計算は一度だけで済み、以降の同じクエリの実行結果はキャッシュから直接返せます。
クエリの種類によっては、これにより ClickHouse server のレイテンシとリソース使用量を大幅に削減できます。
背景、設計、および制限事項
- トランザクション整合性のあるキャッシュでは、
SELECTクエリの結果が変化した場合、または変化する可能性がある場合に、データベースがキャッシュされたクエリ結果を無効化 (破棄) します。ClickHouse では、データを変更する操作には、table への insert/update/delete や、折りたたみ merge が含まれます。トランザクション整合性のあるキャッシュは、たとえば MySQL (クエリキャッシュ は v8.0 以降で削除) や Oracle のような OLTP データベースに特に適しています。 - トランザクション整合性のないキャッシュでは、すべてのキャッシュエントリに有効期間が設定され、期限切れになると失効する (たとえば 1 分) こと、さらにその期間中は基盤となるデータがほとんど変化しないことを前提として、クエリ結果に多少の不正確さが生じることを許容します。この方式は、全体として OLAP データベースにより適しています。トランザクション整合性のないキャッシュで十分な例として、複数のユーザーが同時に利用するレポートツールの時間別売上レポートを考えてみてください。通常、売上データの変化は十分に緩やかなため、データベースはレポートを 1 回だけ計算すればよく (最初の
SELECTクエリがこれに相当します) 、以降のクエリはクエリキャッシュから直接返せます。この例では、妥当な有効期間は 30 分です。
設定と使用方法
clickhouse-local は一度に 1 つのクエリしか実行できません。単一のプロセスでクエリ結果をメモリ上にキャッシュしても意味がないため、
clickhouse-local ではインメモリのクエリキャッシュは無効になっています。一方、ディスク上のクエリキャッシュ は利用できます。そのエントリはディスク上の
ファイルシステムキャッシュに格納されるため、ある clickhouse-local プロセスがキャッシュした結果を、同じファイルシステムキャッシュディレクトリを使用する後続のプロセスで再利用できます。
use_query_cache = true も指定している場合) 、計算済みの結果をキャッシュから読み取り、即座に返します。
use_query_cache の設定およびそのほかのすべてのクエリキャッシュ関連設定は、単独の SELECT ステートメントに対してのみ有効です。特に、
CREATE VIEW AS SELECT [...] SETTINGS use_query_cache = true によって作成されたビューに対する SELECT の結果は、その SELECT
ステートメントを SETTINGS use_query_cache = true 付きで実行しない限り、キャッシュされません。true) を使って、より詳細に構成できます。前者の設定は
クエリ結果をキャッシュに保存するかどうかを制御し、後者の設定は、データベースがクエリ
結果をキャッシュから取得しようとするかどうかを決定します。たとえば、次のクエリはキャッシュを受動的にのみ使用します。つまり、キャッシュから読み取ろうとはしますが、その
結果は保存しません。
use_query_cache、enable_writes_to_query_cache、および
enable_reads_from_query_cache は特定のクエリに対してのみ指定することを推奨します。ユーザーまたはプロファイルレベルでキャッシュを有効にすることも可能です (例: SET use_query_cache = true) が、その場合はすべての SELECT クエリがキャッシュされた結果を返す可能性がある点に留意してください。
クエリキャッシュは、ステートメント SYSTEM CLEAR QUERY CACHE を使用してクリアできます。クエリキャッシュの内容は、システムテーブル
system.query_cache に表示されます。データベースの起動以降のクエリキャッシュのヒット数とミス数は、システムテーブル
system.events のイベント
“QueryCacheHits” および “QueryCacheMisses” として確認できます。どちらのカウンターも、
SELECT クエリが設定 use_query_cache = true で実行された場合にのみ更新され、その他のクエリは “QueryCacheMisses” に影響しません。これらのカウンターはクエリ
キャッシュ全体を対象とします。ディスク上のクエリキャッシュが有効な場合 (下記を参照)、メモリ上でミスした後にディスクから結果が返されたクエリは
1 回の “QueryCacheHits” として、両方のバックエンドでミスしたクエリは 1 回の “QueryCacheMisses” としてカウントされます。フィールド query_cache_usage
は、システムテーブル system.query_log にあり、実行された各クエリについて、そのクエリ結果がクエリキャッシュに書き込まれたか、または
クエリキャッシュから読み取られたかを示します。システムテーブル
system.metrics のメトリクス QueryCacheEntries および QueryCacheBytes は、インメモリのクエリキャッシュに現在含まれているエントリ数 / バイト数を示します。これらには
ディスク上のクエリキャッシュ のエントリは含まれません。ディスク上のクエリキャッシュには、現時点で使用量を示すメトリクスはありません。
クエリキャッシュは ClickHouseサーバープロセスごとに 1 つ存在します。ただし、キャッシュ結果はデフォルトではユーザー間で共有されません。これは
変更できます (下記を参照) が、セキュリティ上の理由から推奨されません。
クエリ結果は、クエリキャッシュ内でそのクエリの Abstract Syntax Tree (AST) によって参照されます。
つまり、キャッシュでは大文字・小文字は区別されず、たとえば SELECT 1 と select 1 は同じクエリとして扱われます。
マッチングをより自然にするため、クエリキャッシュおよび出力フォーマット)に関連するクエリレベルの設定はすべて
AST から除去されます。
例外またはユーザーによるキャンセルによってクエリが中断された場合、クエリキャッシュにエントリは書き込まれません。
クエリキャッシュのサイズ (バイト単位)、cache エントリの最大数、および個々の cache エントリの最大サイズ (バイト単位および
レコード数) は、各種サーバー設定オプションで設定できます。
users.xml のユーザープロファイルに
query_cache_max_size_in_bytes と
query_cache_max_entries の設定を追加し、その後、両方の設定を
読み取り専用にします。
Age および Expires ヘッダーに、キャッシュされたエントリの経過時間 (秒単位) と有効期限のタイムスタンプを設定します。
クエリキャッシュ内のエントリは、デフォルトで圧縮されています。これにより、クエリキャッシュへの書き込み / 読み取りは遅くなるものの、全体的なメモリ消費量は削減されます
。圧縮を無効にするには、設定 query_cache_compress_entries を使用します。
同じクエリに対して複数の結果をキャッシュしておくことが有用な場合があります。これは、
クエリキャッシュの cache エントリに対するラベル (またはネームスペース) として機能する設定 query_cache_tag を使うことで実現できます。クエリキャッシュは、
同じクエリであってもタグが異なる結果は別のものとして扱います。
同じクエリに対して 3 つの異なるクエリキャッシュの cache エントリを作成する例:
tag の付いたエントリのみを削除するには、ステートメント SYSTEM CLEAR QUERY CACHE TAG 'tag' を使用します。
サブクエリのキャッシュ
デフォルトでは、外側のクエリでuse_query_cache を有効にしても、その設定はサブクエリには伝播しません。つまり、各サブクエリで明示的にキャッシュを有効にする必要があります:
query_cache_for_subqueries を使用します。
use_query_cache = false を設定します。
is_subquery = 1 のエントリとして確認できます。ディスク上のクエリキャッシュ が有効な場合、サブクエリの結果はディスクにもキャッシュされますが、これらのエントリは system.query_cache には表示されません。query_cache_ttl 設定はサブクエリの cache エントリにも適用され、サブクエリごとに設定できます。
ClickHouse は、テーブルデータを max_block_size 行ごとのブロックで読み取ります。フィルタリングや集約、
その他の処理の影響により、結果ブロックは通常 ‘max_block_size’ よりかなり小さくなりますが、これよりずっと大きくなる場合もあります。設定
query_cache_squash_partial_results (デフォルトで有効) は、結果ブロックを
クエリ結果キャッシュに挿入する前に、 (非常に小さい場合は) まとめるか、 (大きい場合は) ‘max_block_size’ サイズのブロックに分割するかを制御します。
これによりクエリキャッシュへの書き込み性能は低下しますが、cache エントリの圧縮率が向上し、後でクエリ結果がクエリキャッシュから返される際の
ブロック粒度がより自然になります。
その結果、クエリキャッシュには各クエリごとに複数の (部分的な)
結果ブロックが保存されます。この挙動はデフォルトとしては適切ですが、設定
query_cache_squash_partial_results を使用して無効にできます。
また、非決定論的関数を含むクエリの結果は、デフォルトではキャッシュされません。こうした関数には次のものが含まれます。
- Dictionaries にアクセスする関数:
dictGet()など - XML の
定義内にタグ
<deterministic>true</deterministic>がない user-defined functions - 現在の日付または時刻を返す関数:
now(),today(),yesterday()など - ランダムな値を返す関数:
randomString(),fuzzBits()など - クエリ処理で使われる内部 chunk のサイズや順序に結果が依存する関数:
nowInBlock()など,rowNumberInBlock(),runningDifference(),blockSize()など - 環境に依存する関数:
currentUser(),queryID(),getMacro()など
ディスク上のクエリキャッシュ
デフォルトでは、クエリキャッシュはエントリをメモリ上に保持します。加えて、 ファイルシステムキャッシュをバックエンドとして、クエリ結果をディスク上にキャッシュすることもできます。インメモリのクエリキャッシュと比べて、ディスク上のクエリキャッシュは より多くの容量を使用でき、サーバーを再起動しても失われません。エントリは、それを書き込んだサーバービルドと同じビルドからのみ提供されます。そのため、 アップグレードやダウングレードを行うと、以前のビルドで書き込まれたエントリはキャッシュミスとなり、いずれ上書きされるか追い出されます。 ディスク上のクエリキャッシュを使用するには、まずサーバー設定のfilesystem_caches セクションでファイルシステムキャッシュを設定します。
query_cache_on_disk_cache_name で指定します:
enable_reads_from_query_cache_on_disk および enable_writes_to_query_cache_on_disk (いずれもデフォルトは true
) により、読み取りと書き込みをそれぞれ個別に有効化できます。両方のキャッシュで読み取りが有効な場合、ルックアップはまずメモリで行われ、ミスした場合にのみディスクで行われます。両方のキャッシュで書き込み
が有効な場合、結果は両方に保存されます。結果を保存できるキャッシュが存在しない場合 (例:インメモリのクエリキャッシュを持たない clickhouse-local で
enable_writes_to_query_cache_on_disk = false とした場合) でも、非決定論的
関数、システムテーブル、または throw 以外のオーバーフローモードを理由にクエリが拒否されることはありません。これらのチェックは、誤った結果の保存を防ぐためだけのものだからです。ディスク上のエントリは、設定
query_cache_on_disk_codec (デフォルトは ZSTD(3)) で指定されたコーデックで圧縮されます。クエリキャッシュのサーバー設定 max_entry_size_in_bytes および max_entry_size_in_rows は、
両方のバックエンドのエントリを制限します。各バックエンドはエントリを自身の保存形式に基づいて計測するため、ディスク上ではバイト数の上限は
シリアル化・圧縮後のエントリに適用されます。いずれかを 0 に設定すると、両方のバックエンドへの書き込みが無効になります。その他の
すべてのクエリキャッシュ設定 (例:query_cache_ttl、query_cache_min_query_duration) は、以下の 2 つの例外を除き、
インメモリのクエリキャッシュと同様にディスク上のクエリキャッシュにも適用されます。
設定 query_cache_max_size_in_bytes および query_cache_max_entries は、インメモリのクエリキャッシュにのみ適用されるユーザーごとの上限です。これらは
ディスク上のクエリキャッシュには適用されません。ディスク上のエントリの上限は、設定されたファイルシステムキャッシュのサイズによって決まり、そのルールに従って追い出されます
(後述) 。そのため、制限の厳しい設定プロファイルを持つユーザーであっても、これらの設定でメモリ上に許容される量を超えて結果をディスクに保存できます。
これは意図的な仕様です。ファイルシステムキャッシュはサイズ上限のある共有リソースであり、ディスク上のクエリキャッシュはその中に領域を予約しません。
設定 query_cache_share_between_users も例外です。この設定は両方のバックエンドで同じ意味を持ち、この設定を有効にして書き込まれたエントリは
すべてのユーザーに提供されます。ただし、非共有エントリの分離方法が異なります。インメモリのクエリキャッシュはクエリごとに単一のエントリを保持し、
読み取り時にそのエントリが別のユーザーのものであれば拒否します。そのため、ユーザー A の非共有エントリは、期限切れになるか
追い出されるまで、ユーザー B の書き込みを妨げます。一方、ディスク上のクエリキャッシュでは、ユーザーとそのユーザーの現在のロールの組み合わせを非共有エントリのキーの一部とするため、
ユーザー B はユーザー A のエントリにはヒットせず、結果のコピーを独自に保存します。これは、同一ユーザーが異なる現在のロールの組み合わせで
実行する場合 (例:SET ROLE の後) にも当てはまります。ロールは権限と行ポリシーを伴うため、異なるロールの下で計算された結果は
別々にキャッシュされます。
ディスク上のクエリキャッシュのエントリは、ファイルシステムキャッシュの通常のエントリであり、同じファイルシステムキャッシュにキャッシュされている他のデータと区別されません。つまり、削除から保護されることはなく、
ファイルシステムキャッシュの容量が不足した場合には同じルール (例: least-recently-used) に従って追い出されます。合計サイズや件数に対する個別の制限はなく、ファイルシステム
キャッシュ自体の制限のみが適用されます。ステートメント SYSTEM CLEAR QUERY CACHE (および SYSTEM CLEAR QUERY CACHE TAG 'tag') は、セッションの設定 query_cache_on_disk_cache_name で選択されたファイルシステムキャッシュ
(例: SET query_cache_on_disk_cache_name = 'query_results' の実行後) から、ディスク上のクエリキャッシュのエントリ (指定された
タグを持つもの。ただし、破損したエントリおよび互換性のないフォーマットのエントリはタグに関係なく削除されます) も削除し、ファイルシステムキャッシュ内のその他のデータはそのまま残します。
エントリを見つけるために、ファイルシステムキャッシュのすべてのファイルセグメントのメタデータを (ディスクから読み取ることなくメモリ上で) 走査し、
クエリキャッシュのエントリとしてマークされた各キーのヘッダーを読み取ります。この走査にかかる時間は
ファイルシステムキャッシュ内のファイルセグメント数に比例するため、クエリ結果専用のファイルシステムキャッシュを用意しておけば、SYSTEM CLEAR QUERY CACHE のコストを低く抑えられます。
ファイルシステムキャッシュがエントリの先頭部分をすでに追い出している場合、エントリの残りの部分は、ファイルシステムキャッシュが
それを追い出すか、同じクエリ結果が再度書き込まれるまで残り続けますが、結果として返されることはありません。書き込み中の
エントリも同様にそのまま残されます。ステートメント SYSTEM DROP FILESYSTEM CACHE '<name>' を実行すると、これらのエントリはファイルシステムキャッシュ内の他のすべてのデータと
ともに削除されます。システムテーブル
system.query_cache およびメトリクス QueryCacheEntries と QueryCacheBytes に表示されるのは、
インメモリのクエリキャッシュのエントリのみです。ディスク上のクエリ
キャッシュのヒット数とミス数は、システムテーブル
system.events のイベント “QueryCacheOnDiskHits” および “QueryCacheOnDiskMisses” として表示されます。これらはディスク上のバックエンドのみの内訳です。一方、“QueryCacheHits” と
“QueryCacheMisses” は引き続きクエリキャッシュ全体のカウンターであり、“QueryCacheAgeSeconds” は両方のバックエンドのヒットを集計します。
メモリ内キャッシュが先に参照されるため、メモリ内のヒット数は “QueryCacheHits” から “QueryCacheOnDiskHits” を引いた値になります。