用可控的新鲜度窗口复用昂贵 SELECT 的结果,换取更低延迟与更低计算成本。

核心问题

很多分析查询第一次执行时很贵,但第二次、第三次执行时,真正变化的往往不是 SQL 本身,而只是“结果是否还足够新”。查询结果缓存要解决的就是这个问题:当同一条查询被反复执行时,是否可以直接复用已经算好的结果,而不是每次重算。

一致性光谱

查询结果缓存大致有两种思路:

  • 事务一致缓存:底层数据一变化,相关缓存就失效,适合正确性要求极高的 OLTP 场景。
  • 事务不一致缓存:接受短时间内的轻微过期,用 TTL 控制结果有效期,更适合 OLAP 或报表场景。

clickhouse-query-cache 说明 ClickHouse 明确选择了第二条路线:它不试图在每次写入后精确失效所有相关缓存,而是承认分析型数据库更常见的诉求是“多数时候结果差几秒或几十秒也可以”。

四个关键设计轴

1. 准入

不是所有查询都值得缓存。合理的系统通常会限制:

  • 只有运行足够慢的查询才可缓存;
  • 只有重复执行达到一定次数的查询才可缓存;
  • 单条结果、总缓存大小、用户占用量都需要上限。

这能避免缓存被一次性、低收益的查询挤满。

2. 新鲜度

TTL 决定了缓存能“旧”到什么程度。过短,命中率不高;过长,又会让结果偏离真实数据太多。对于报表类 OLAP 任务,合适的 TTL 本质上是在延迟、成本与正确性之间做业务化权衡。

3. 匹配与命名空间

结果缓存不仅要判断“是不是同一条查询”,还要判断“是不是同一个语义上下文”。ClickHouse 用 AST 做匹配,并提供 query_cache_tag 作为命名空间,这是一种很实用的折中:

  • 语法层面的小差异不会导致意外失配;
  • 需要区分不同报表上下文时,又能显式切开缓存空间。

4. 安全与可复用性

结果缓存天然有权限风险:若不同用户看到的数据范围不同,共享缓存可能造成越权读取。因此,安全默认值通常应该倾向于:

  • 不缓存非确定性结果;
  • 不缓存依赖系统状态的查询;
  • 默认不跨用户共享缓存。

从 ClickHouse 的演化看这个主题

introducing-the-clickhouse-query-cache 提供了一个很有价值的补充:查询结果缓存不只是“功能说明”,还是一个会经历实验态 → 正式文档化 → 运维细化的产品能力。

在 ClickHouse 的早期博客里,这个功能强调的是:

  • 如何通过一条真实慢查询把命中前后的收益展示出来;
  • 如何通过 system.query_cachesystem.query_log 与 trace 日志排查“为什么没命中”;
  • 为什么要选择事务不一致缓存,而不是追求写后精确失效。

而在后来的正式文档 clickhouse-query-cache 中,重点则更偏向:

  • 各类设置项的稳定命名;
  • 用户隔离、系统表、非确定性函数等默认边界;
  • 更完整的清理、标签命名空间与可观测性接口。

这提醒我:理解一个缓存系统,不能只看“现在怎么配”,还要看它最初想解决什么问题,以及为什么后来把接口收敛成现在这个样子。

适用场景

查询结果缓存最适合这些情况:

  • 同一条报表或聚合查询被多人重复打开;
  • 底层数据变化相对平缓;
  • 第一次执行很贵,但后续重复查询的商业价值很高;
  • 可以接受一个明确的新鲜度窗口。

反过来说,如果业务要求每次读取都必须严格反映最新写入,或者查询本来就很快,那么结果缓存往往不是首选。

不要和索引混为一谈

sql-indexing 优化的是第一次执行时的数据访问路径,而查询结果缓存优化的是后续重复执行时的重算成本。两者经常都能改善性能,但作用点不同:

  • 索引让数据库更快找到数据;
  • 结果缓存让数据库在合适时直接跳过重复计算。

因此,结果缓存通常不是索引的替代品,而是另一条性能杠杆。

实践检查表

  • 先按具体查询开启,不要一上来全局开启;
  • 先监控命中率、缓存字节数与淘汰情况,再决定是否扩大范围;
  • 让 TTL 由业务新鲜度要求驱动,而不是拍脑袋;
  • 明确哪些函数、系统表或权限边界不该进入缓存。

来源:clickhouse-query-cache · introducing-the-clickhouse-query-cache

相关页面:clickhouse · sql-indexing · clickhouse-query-cache · introducing-the-clickhouse-query-cache