分析ワークロードでは、変更されていないデータに対して同じ集約クエリが繰り返し実行されることがよくあります。例えば:
SELECT region, SUM(revenue) FROM orders WHERE dt = '2024-01-01' GROUP BY region; SELECT region, SUM(revenue) FROM orders WHERE dt = '2024-01-01' GROUP BY region;
各実行では、同一のタブレットを再スキャンし、同一の集約結果を再計算するため、CPUとI/Oリソースが無駄になります。
これを解決するために、Apache DorisはQuery Cacheメカニズムを提供しています。パイプライン実行エンジン内で生成された中間集約結果をキャッシュし、同一の実行コンテキストを共有する後続のクエリに直接提供することで、クエリレイテンシを大幅に削減します。
:::caution 重要な制限事項
Query Cacheは集約クエリ用に設計されています。具体的には、プランツリーが以下のパターンのいずれかと一致するフラグメントのみが対象となります:
AggregationNode → OlapScanNode(スキャンでの直接単一フェーズ集約)AggregationNode → AggregationNode → OlapScanNode(スキャンでの二段階集約)集約ノードとスキャンノード間には、FilterNodeやProjectNodeなどの中間ノードが許可されます。ただし、プランツリーは、キャッシュ対象のサブツリー内にJoinNode、SortNode、UnionNode、WindowNode、またはExchangeNodeを含んではいけません。
キャッシュキーは3つの部分で構成されます:
SQL Digest — 正規化されたプランツリー(集約関数、グループ化式、非パーティション フィルタ述語、投影、および結果に影響するセッション変数)から計算されたSHA-256ハッシュ。正規化プロセスでは、すべての内部識別子に標準IDが割り当てられるため、意味的に同一の2つのクエリは、異なる内部プランノード/スロットIDを持つ場合でも、同じダイジェストを生成します。
Tablet ID — 現在のパイプラインインスタンスに割り当てられたタブレットIDのソート済みリスト。
Tablet Range — パーティション述語から導出される各タブレットの有効スキャン範囲(パーティションとフィルタの動作を参照)。
以下のいずれかが発生すると、キャッシュエントリは無効になります:
query_cache_force_refresh = trueの場合、キャッシュされた結果は無視され、クエリが再実行されます。初回実行(キャッシュミス):
後続実行(キャッシュヒット):
SELECT a, b対SELECT b, a)、列は自動的に並べ替えられます。パーティション述語とフィルタ式がQuery Cacheとどのように相互作用するかを理解することは、良好なヒット率を達成するために不可欠です。
単一列RANGEパーティショニングを持つテーブルでは、パーティション述語は特別な扱いを受けます:
例:
dtによる日次パーティションを持つテーブルordersを考えます:
-- Query A SELECT region, SUM(revenue) FROM orders WHERE dt >= '2024-01-01' AND dt < '2024-01-03' GROUP BY region; -- Query B SELECT region, SUM(revenue) FROM orders WHERE dt >= '2024-01-02' AND dt < '2024-01-04' GROUP BY region;
2024-01-01と2024-01-02からタブレットをスキャンします。2024-01-02と2024-01-03からタブレットをスキャンします。2024-01-02のタブレットは同じdigestと同じタブレット範囲を持つため、Query Bは2024-01-02パーティションに対してQuery Aのキャッシュを再利用できます。パーティション2024-01-03のみ新しく計算する必要があります。マルチカラムRANGEパーティション、LISTパーティション、またはUNPARTITIONEDテーブルの場合、パーティション述語を抽出できないため、digestに直接含まれます。この場合、パーティション述語のわずかな違いでも異なるdigestが生成され、キャッシュミスが発生します。
非パーティションフィルター式(例:WHERE status = 'active')は正規化されたplan digestに含まれます。2つのクエリがキャッシュエントリを共有できるのは、正規化後に非パーティションフィルター式が意味的に同一である場合のみです。
WHERE status = 'active'とWHERE status = 'active' — 同じdigest、キャッシュヒット。WHERE status = 'active'とWHERE status = 'inactive' — 異なるdigest、キャッシュミス。WHERE status = 'active' AND region = 'ASIA'とWHERE region = 'ASIA' AND status = 'active' — 正規化プロセスが連言をソートするため、同じdigestが生成され、キャッシュヒットできます。クエリ結果に影響するセッション変数(time_zone、sql_mode、sql_select_limitなど)はdigestに含まれます。クエリ間でこれらの変数を変更すると、異なるキャッシュキーが生成され、キャッシュミスが発生します。
以下の条件により、plannerはフラグメントに対してQuery Cacheを完全にスキップします:
| 条件 | 理由 |
|---|---|
| フラグメントがランタイムフィルターの対象 | ランタイムフィルター値は動的でplan時に不明。キャッシュすると不正な結果が生成される |
非決定的式(rand()、now()、uuid()、UDFなど) | 同一の入力でも実行毎に結果が変わる |
| プランのキャッシュサブツリーにJOIN、SORT、UNION、WINDOWノードが含まれる | aggregation-over-scanパターンのみサポート |
スキャンノードがOlapScanNodeでない(例:外部テーブルスキャン) | キャッシュはタブレットIDとバージョンに依存するが、外部テーブルには存在しない |
Query Cacheは内部OLAPテーブル固有の3つのプロパティに依存しています:
タブレットベースのデータ構成 — キャッシュキーにはタブレットIDとタブレット毎のスキャン範囲が含まれます。外部テーブルは外部システム(HDFS、S3、JDBCなど)にデータを格納し、タブレットの概念がありません。
バージョンベースの無効化 — 各内部タブレットには単調増加するバージョン番号があり、データ変更時に変更されます。キャッシュはこのバージョンを使用して古いデータを検出します。外部テーブルはDorisにそのようなバージョニングを公開しません。
OlapScanNode要件 — plan正規化ロジックは、aggregationキャッシュポイント下の有効なスキャンノードとしてOlapScanNodeのみを認識します。外部テーブルスキャンノードは認識されません。
外部テーブルでのキャッシュニーズについては、代わりにSQL Cacheの使用を検討してください。
| パラメータ | 説明 | デフォルト |
|---|---|---|
enable_query_cache | Query Cacheを有効または無効にするマスタースイッチ | false |
query_cache_force_refresh | trueの場合、キャッシュされた結果を無視してクエリを再実行。新しい結果は依然としてキャッシュに書き込まれる | false |
query_cache_entry_max_bytes | 単一キャッシュエントリの最大サイズ(バイト)。aggregation結果がこの制限を超える場合、そのフラグメントのキャッシュは放棄される | 5242880 (5 MB) |
query_cache_entry_max_rows | 単一キャッシュエントリの最大行数。aggregation結果がこの制限を超える場合、そのフラグメントのキャッシュは放棄される | 500000 |
| パラメータ | 説明 | デフォルト |
|---|---|---|
query_cache_size | 各BE上のQuery Cacheの総メモリ容量(MB) | 512 |
:::note be.confのパラメータquery_cache_max_size_mbとquery_cache_elasticity_size_mbは、ここで説明しているパイプラインレベルのQuery Cacheではなく、古いSQL Result Cacheを制御します。両者を混同しないでください。 :::
SET enable_query_cache = true;
-- First execution: cache miss, results are computed and cached SELECT region, SUM(revenue), COUNT(*) FROM orders WHERE dt = '2024-01-15' AND status = 'completed' GROUP BY region; -- Second execution: cache hit, results are served directly from cache SELECT region, SUM(revenue), COUNT(*) FROM orders WHERE dt = '2024-01-15' AND status = 'completed' GROUP BY region;
クエリを実行した後、クエリ profile を確認してください。CacheSourceOperator セクションを探します:
HitCache: true — クエリはキャッシュから結果を提供しました。HitCache: false, InsertCache: true — クエリはキャッシュをミスしましたが、結果の挿入に成功しました。HitCache: false, InsertCache: false — クエリはキャッシュをミスし、結果が大きすぎてキャッシュできませんでした。Profile には、どの tablet が関与したかを示す CacheTabletId も表示されます。
-- Force the next query to bypass cache and re-compute results SET query_cache_force_refresh = true; SELECT region, SUM(revenue) FROM orders WHERE dt = '2024-01-15' GROUP BY region; -- Reset SET query_cache_force_refresh = false;
Query Cacheは以下のケースで最も効果的です:
Query Cacheは以下には適していません:
now()、rand()、uuid()、およびUDFはキャッシュを無効にします。query_cache_sizeを調整してください。Query CacheはDorisにおけるパイプラインレベルの最適化メカニズムで、tabletごとに中間集計結果をキャッシュします。主な特徴: