跳到主要内容

Query Cache(查询缓存)

Query Cache 是 Apache Doris 流水线执行引擎中按 Tablet 粒度缓存中间聚合结果的机制,用于加速重复的聚合查询。

阅读前 Checklist

在使用 Query Cache 前,请确认:

  • 查询的是 内部 OLAP 表(非 Hive/JDBC/Iceberg/Hudi/Paimon 等外部表)
  • 查询是 聚合查询(包含 GROUP BY 或聚合函数)
  • 查询计划符合 AggregationNode → OlapScanNode 模式
  • 查询不包含 JOINSORTUNIONWINDOW 节点
  • 查询不依赖 now()rand()uuid() 等非确定性函数
  • 已设置 enable_query_cache = true

一句话定义

Query Cache 在流水线执行引擎中按 Tablet 粒度缓存聚合结果,当后续查询的执行上下文相同时直接返回缓存数据,避免重复扫描和重复计算。

为什么需要 Query Cache

在分析型场景中,同一聚合查询经常被重复执行,但底层数据并未变化。例如:

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;

每次执行都会重新扫描相同 Tablet 并重新计算,浪费 CPU 与 I/O。Query Cache 缓存中间聚合结果,命中后直接返回,大幅降低延迟。

重要限制
  • 仅适用于 内部 OLAP 表上的聚合查询。普通扫描、JOIN、排序等不会使用 Query Cache。
  • 不支持外部表(Hive、JDBC、Iceberg、Hudi、Paimon 等)。

工作原理

支持的查询模式

只有执行计划树(Plan Tree)匹配以下模式的 Fragment 才有资格使用缓存:

  • AggregationNode → OlapScanNode:直接在扫描上进行的单阶段聚合。
  • AggregationNode → AggregationNode → OlapScanNode:在扫描上进行的两阶段聚合。

聚合节点和扫描节点之间允许存在 FilterNodeProjectNode 等中间节点。但缓存子树中 不能 包含 JoinNodeSortNodeUnionNodeWindowNodeExchangeNode

缓存键的三个组成部分

组成部分说明
SQL 摘要基于归一化执行计划树(聚合函数、分组表达式、非分区过滤谓词、投影列、影响结果的 Session 变量)计算的 SHA-256 哈希。语义相同的查询会得到相同摘要
Tablet ID 列表分配给当前 Pipeline 实例的、排序后的 Tablet ID 列表
Tablet 范围每个 Tablet 的有效扫描范围,由分区谓词推导而来(详见分区与过滤行为

缓存失效条件

触发条件说明
数据变更INSERT、DELETE、UPDATE 或 Compaction 使 Tablet 版本号递增;后续查询比对版本,不一致即未命中
Schema 变更ALTER TABLE 改变表结构,从而改变执行计划与摘要
LRU 淘汰缓存内存超限时,按 LRU-K(K=2)淘汰;新条目须至少被访问两次才能被准入
过期清理超过 24 小时的条目由周期性清理任务自动移除
强制刷新设置 query_cache_force_refresh = true 时忽略缓存并重新执行

执行流程

首次执行(缓存未命中):

  1. 扫描算子正常从 Tablet 中读取数据。
  2. 聚合算子计算结果。
  3. 结果发送给下游消费者,同时累积以准备写入缓存。
  4. 执行完成后,若累积结果未超过单条目大小/行数限制,结果将被写入缓存。

后续执行(缓存命中):

  1. 扫描算子检测到缓存命中,跳过扫描范围——不读取任何 Tablet 数据。
  2. 聚合算子无输入,无输出。
  3. 缓存源算子直接提供缓存的数据块。
  4. 若列顺序与缓存条目不同(例如 SELECT a, bSELECT b, a 摘要相同),列会被自动重新排列。

分区与过滤行为

理解分区谓词与过滤表达式如何与 Query Cache 交互,对获得高命中率至关重要。

单列 RANGE 分区谓词

对于 单列 RANGE 分区 表,分区谓词会被特殊处理:

  • 分区谓词从摘要中 被提取出来;系统会计算谓词范围与每个分区实际范围边界的交集,作为 Tablet 范围字符串附加到缓存键中。
  • 两个仅在分区过滤范围上有差异的查询,可在共同 Tablet 上 共享缓存

示例:表 ordersdt 列每日分区。

-- 查询 A
SELECT region, SUM(revenue) FROM orders
WHERE dt >= '2024-01-01' AND dt < '2024-01-03' GROUP BY region;

-- 查询 B
SELECT region, SUM(revenue) FROM orders
WHERE dt >= '2024-01-02' AND dt < '2024-01-04' GROUP BY region;
  • 查询 A 扫描分区 2024-01-012024-01-02
  • 查询 B 扫描分区 2024-01-022024-01-03
  • 分区 2024-01-02 的 Tablet 摘要与范围相同,因此 查询 B 复用查询 A 在 2024-01-02 的缓存,仅需重新计算 2024-01-03 分区。

多列 RANGE / LIST / 未分区表

对于 多列 RANGE 分区LIST 分区未分区 的表,分区谓词无法被提取,会被直接包含在摘要中。即使分区谓词只有微小差异,也会产生不同摘要并导致缓存未命中。

非分区过滤表达式

非分区过滤表达式(如 WHERE status = 'active')会被包含在归一化的执行计划摘要中。仅当两个查询的过滤表达式归一化后语义完全相同时,才能共享缓存。

查询 1查询 2是否共享缓存
WHERE status = 'active'WHERE status = 'active'是(相同摘要)
WHERE status = 'active'WHERE status = 'inactive'否(不同摘要)
WHERE status = 'active' AND region = 'ASIA'WHERE region = 'ASIA' AND status = 'active'是(归一化后顺序无关)

Session 变量

影响查询结果的 Session 变量(如 time_zonesql_modesql_select_limit)会被包含在摘要中。在两次查询之间更改任一变量都会产生不同缓存键并导致未命中。

导致 Query Cache 被禁用的条件

条件原因
Fragment 是 Runtime Filter 的目标Runtime Filter 值在规划时未知,缓存会产生错误结果
包含非确定性表达式(rand()now()uuid()、UDF 等)即使输入相同,结果也会因执行次数不同而变化
缓存子树中包含 JOIN、SORT、UNION 或 WINDOW 节点仅支持「聚合-扫描」模式
扫描节点不是 OlapScanNode(例如外部表扫描)缓存依赖 Tablet ID 与版本,外部表不存在这些概念

Query Cache 不支持外部表的原因

Query Cache 依赖内部 OLAP 表的三个特有属性:

  1. 基于 Tablet 的数据组织:缓存键包含 Tablet ID 和每个 Tablet 的扫描范围;外部表存储在 HDFS、S3、JDBC 等外部系统中,没有 Tablet 概念。
  2. 基于版本的失效机制:每个内部 Tablet 都有单调递增的版本号,缓存以此检测过期;外部表不向 Doris 暴露此版本机制。
  3. OlapScanNode 要求:执行计划归一化逻辑只识别 OlapScanNode 作为聚合缓存点下方的有效扫描节点。

外部表的缓存需求请改用 SQL Cache

配置参数

Session 变量(FE)

参数说明默认值
enable_query_cache启用或禁用 Query Cache 的总开关false
query_cache_force_refresh设为 true 时忽略缓存结果并重新执行查询;新结果仍会写入缓存false
query_cache_entry_max_bytes单个缓存条目的最大字节数;超过该限制的 Fragment 结果不会被缓存5242880(5 MB)
query_cache_entry_max_rows单个缓存条目的最大行数;超过该限制的 Fragment 结果不会被缓存500000

BE 配置(be.conf)

参数说明默认值
query_cache_size每个 BE 上 Query Cache 的总内存容量(MB)512
备注

be.conf 中的 query_cache_max_size_mbquery_cache_elasticity_size_mb 控制的是旧版 SQL Result Cache,不是 本文描述的流水线级别 Query Cache,请勿混淆。

使用示例

步骤 1:开启 Query Cache

目的:启用 Query Cache 总开关。

SET enable_query_cache = true;

说明:该变量为 Session 级,需要在每个连接中开启;可在 FE 全局变量中设置默认值。

步骤 2:执行典型聚合查询

目的:触发缓存写入与读取。

-- 第一次执行:缓存未命中,计算结果并写入缓存
SELECT region, SUM(revenue), COUNT(*)
FROM orders
WHERE dt = '2024-01-15' AND status = 'completed'
GROUP BY region;

-- 第二次执行:缓存命中,直接从缓存返回结果
SELECT region, SUM(revenue), COUNT(*)
FROM orders
WHERE dt = '2024-01-15' AND status = 'completed'
GROUP BY region;

说明:第二次执行的 SQL 摘要、Tablet ID 列表和 Tablet 范围与第一次完全一致,因此命中缓存。

步骤 3:通过 Profile 验证命中

目的:确认查询是否真的使用了缓存。

执行查询后查看 Profile,定位 CacheSourceOperator 部分:

Profile 字段含义
HitCache: true查询从缓存中获取了结果
HitCache: falseInsertCache: true未命中,但成功将结果写入缓存
HitCache: falseInsertCache: false未命中,且结果过大无法缓存
CacheTabletId缓存涉及的 Tablet ID

步骤 4:强制刷新缓存

目的:忽略已有缓存并重算结果(如怀疑缓存数据异常)。

-- 强制下一次查询跳过缓存并重新计算结果
SET query_cache_force_refresh = true;

SELECT region, SUM(revenue) FROM orders WHERE dt = '2024-01-15' GROUP BY region;

-- 重置
SET query_cache_force_refresh = false;

说明:强制刷新后新结果仍会写入缓存。

适用场景对比

场景是否适用原因
仪表板/BI 工具反复执行相同聚合 SQL适用摘要与 Tablet 完全一致,命中率高
T+1 报表(数据每天加载一次)适用当天后续查询可命中缓存
重叠日期范围的聚合查询适用单列 RANGE 分区可在 Tablet 级别共享缓存条目
普通 SELECT 扫描、JOIN、排序、窗口函数不适用仅支持「聚合-扫描」模式
外部表(Hive、JDBC、Iceberg、Hudi、Paimon)不适用无 Tablet 与版本机制;建议改用 SQL Cache
频繁更新的表不适用Tablet 版本快速变化,命中率低
now()/rand()/uuid()/UDF 的查询不适用非确定性结果,缓存被禁用
依赖 Runtime Filter 的查询不适用Runtime Filter 值在规划时未知

注意事项

  • 缓存非持久化:Query Cache 驻留在 BE 内存中,BE 重启后缓存被清空。
  • 内存消耗:缓存数据块占用 BE 内存,请监控内存并按需调整 query_cache_size
  • LRU-K 准入机制:缓存已满时,新条目须至少被访问两次才能被准入(K=2),可防止低频查询污染缓存。

故障排查(Troubleshooting)

现象可能原因解决方案
HitCache: false 持续出现未开启 enable_query_cacheSET enable_query_cache = true
HitCache: falseInsertCache: false单条目结果过大,超过 query_cache_entry_max_bytes_max_rows增大对应阈值或增加过滤条件减少结果
计划中找不到 CacheSourceOperator计划包含 JOIN/SORT/UNION/WINDOW,或为 Runtime Filter 目标改写 SQL 使其匹配「聚合-扫描」模式
表是外部表Query Cache 不支持外部表使用 SQL Cache
数据未变化但仍未命中Schema 变更、Session 变量变化、query_cache_force_refresh = true检查 ALTER 历史、对比 Session 变量、重置 query_cache_force_refresh
缓存命中率很低Tablet 频繁更新或 Compaction 频繁调整写入频率,或对低更新表启用
BE 内存压力升高query_cache_size 设置过大降低 query_cache_size 并重启 BE

FAQ

Q1:Query Cache 与 SQL Cache 有何区别?

维度Query CacheSQL Cache
缓存粒度Tablet 粒度的中间聚合结果整条 SQL 的最终结果
适用查询仅内部 OLAP 表的聚合查询任意查询(含外部表)
共享能力不同 SQL 可在 Tablet 级别共享缓存仅完全相同的 SQL 文本可命中
失效机制Tablet 版本号变化即失效基于分区版本或时间

Q2:开启后会立刻命中缓存吗?

不会。第一次执行属于「缓存未命中、写入缓存」;从第二次起才有可能命中。此外 LRU-K(K=2)要求新条目至少被访问两次才会被真正准入。

Q3:可以缓存涉及 JOIN 的聚合吗?

不可以。缓存子树中包含 JoinNode 会使 Query Cache 在该 Fragment 上被禁用。可以考虑改写为先聚合再 JOIN,或使用物化视图。

Q4:BE 重启后需要预热吗?

需要。Query Cache 是内存缓存,重启后清空;可在低峰期主动跑一遍核心聚合 SQL 进行预热。

Q5:怎样确认是否真的命中?

执行 SQL 后查看 Profile 中 CacheSourceOperatorHitCache 字段。

总结

Query Cache 是 Doris 流水线级别的优化机制,按 Tablet 粒度缓存中间聚合结果。其核心特点:

  • 仅适用于 内部 OLAP 表上的 聚合查询
  • 基于 Tablet 版本自动进行缓存失效。
  • 智能地将分区谓词从摘要中分离,使分区范围重叠的查询可共享缓存。
  • 提供单条目大小与行数限制,避免过大结果消耗缓存内存。
  • 使用 LRU-K(K=2)淘汰策略维护高质量缓存。