ClickHouse Query Cache:设计与使用

这篇笔记主要基于 ClickHouse 官方博客文章
《Introducing the ClickHouse Query Cache》(2023-02-09)整理,重点看它解决什么问题、怎么用,以及为什么它是“有意设计成不严格一致”的。

Query Cache 缓存的是什么

它缓存的不是数据页,也不是执行计划,而是 SELECT 查询的结果集

也就是说,第一次执行一条较重的查询时,ClickHouse 正常完成计算;如果开启了 Query Cache,那么在缓存 TTL 没过期前,后续相同查询可以直接从缓存返回结果。

这类场景很典型:

  • Grafana / Superset 这类看板会重复请求相同查询
  • 一段时间内数据变化不大
  • 允许结果有轻微延迟
  • 原查询计算代价较高

所以它更像“结果缓存”,而不是“通用加速器”。

一个最关键的适用前提

Query Cache 适合的是:

  • 重复查询
  • 可接受一定时间窗口内的陈旧结果
  • 查询结果集大小适中

不适合的是:

  • 对实时一致性要求很高的查询
  • 结果集特别大的查询
  • 查询文本或结构经常变化、难以重复命中的场景

基本使用方式

官方文章中提到,在当时的版本里 Query Cache 作为实验特性在 v23.1 引入,首先需要开启:

SET allow_experimental_query_cache = true;

然后在查询上显式启用:

SELECT
    repo_name,
    toDate(created_at) AS day,
    count() AS stars
FROM github_events
WHERE event_type = 'WatchEvent'
GROUP BY
    repo_name,
    day
ORDER BY count() DESC
LIMIT 1 BY repo_name
LIMIT 50
SETTINGS use_query_cache = true;

默认情况下,官方文章里给出的缓存 TTL 是 60 秒,也可以通过 query_cache_ttl 调整:

SELECT ...
SETTINGS
    use_query_cache = true,
    query_cache_ttl = 300;

为什么第一次开了缓存,第二次仍然没命中

官方博文里有一个很好的例子:
一条查询第一次执行约 8 秒,第二次还是约 8 秒,看起来像完全没命中缓存。

排查方法有两个:

1. 看 system.query_cache

SELECT *
FROM system.query_cache;

如果这里没有对应记录,说明结果压根没被放进去。

2. 打 trace 日志

文章中通过 trace 日志发现了真正原因:
查询结果太大,超过了默认的单条缓存项大小限制,所以被跳过了。

这是个很实用的提醒:

Query Cache 不是“开关打开就一定生效”,它还受缓存容量、单项大小、记录数等阈值限制。

相关配置项怎么理解

官方文章给出了服务端配置示例:

<query_cache>
    <size>1073741824</size>
    <max_entries>1024</max_entries>
    <max_entry_size>1048576</max_entry_size>
    <max_entry_records>30000000</max_entry_records>
</query_cache>

可以把这几个参数简单理解成:

  • size:整个 Query Cache 可用的总大小
  • max_entries:最多允许多少个缓存项
  • max_entry_size:单个缓存结果最大能有多大
  • max_entry_records:单个缓存项最多允许多少行记录

如果你的查询结果很大,即使 SQL 本身重复率很高,也可能根本进不了缓存。

怎么判断是否命中了 Query Cache

官方文章建议看 system.query_log 中的 ProfileEvents['QueryCacheHits']

SELECT
    query,
    ProfileEvents['QueryCacheHits']
FROM system.query_log
WHERE type = 'QueryFinish';

这是线上排查时很有价值的一步,因为它比“主观感觉变快了没有”更可靠。

它最值得注意的设计:默认接受不严格一致

这是我认为这篇官方文章最重要的部分。

ClickHouse 明确把 Query Cache 设计成 transactionally inconsistent
意思不是“缓存错了”,而是:

  • 插入
  • 更新
  • 删除
  • 内部 housekeeping 操作

默认不会立即让已有缓存失效

这和很多 OLTP 数据库对 Query Cache 的期待不一样。
OLTP 侧更常见的思路是:只要底层数据可能变了,就尽量让缓存失效,保证结果严格一致。

但 ClickHouse 的取舍是另一套逻辑:

  • 它是 OLAP 系统
  • 很多分析查询天然允许短时间的结果陈旧
  • 如果为了严格一致性频繁失效缓存,会把维护成本和并发开销拉高

所以它更偏向:

给缓存一个 TTL,在这段时间里允许结果不完全最新,以换取更低的查询成本和更好的扩展性。

这其实很符合分析系统的现实使用方式,尤其是:

  • 看板
  • 周期性报表
  • 汇总查询
  • “最近一段时间趋势”类接口

它为什么不走 MySQL 式的强失效逻辑

官方文章专门提到,这样的设计是为了避免类似 MySQL Query Cache 在高吞吐场景里的扩展性问题。

这个点非常关键。
很多缓存设计不是“不知道怎样更正确”,而是“知道,但那样在目标场景下不划算”。

ClickHouse 的目标不是提供最严格的一致性缓存,而是提供一种:

  • 更适合分析场景
  • 更便于扩展
  • 更少维护成本

的查询结果缓存。

Query Cache 的 key 是怎么理解的

官方文章提到,ClickHouse 使用查询的 AST 来引用查询结果,而不是直接依赖原始查询文本。

这意味着像下面这种大小写差异:

SELECT 1;
select 1;

会被视为相同查询。

这个细节带来的好处是:
缓存命中不会因为无关紧要的大小写差异而被打碎,行为更自然。

当前阶段可以把它当成什么

如果站在工程实践角度,我会把 Query Cache 理解成:

  • 一个适合重复重查询的结果缓存
  • 一个对一致性做了业务妥协的 OLAP 设计
  • 一个需要配合监控、日志和容量参数一起观察的能力

它不是所有慢 SQL 的通用解法。
如果查询本身写得很差、扫描范围过大、排序键不合理,Query Cache 只能“遮住问题的一部分”,不能替代表设计和 SQL 优化。

和 SQL 优化的关系

在 ClickHouse 里,性能优化大致可以拆成三层:

  1. 表设计是否合理
  2. SQL 是否减少了不必要的扫描和计算
  3. 重复查询能否被缓存复用

Query Cache 主要解决第三层。
所以更合理的使用顺序通常是:

  1. 先把 SQL 本身写对
  2. 再判断该查询是否高频重复
  3. 最后再看是否值得使用 Query Cache

我的实践判断

如果一个查询同时满足下面几点,我会优先考虑它:

  • 查询本身不短
  • 会被固定面板反复执行
  • 结果允许几十秒到几分钟的陈旧
  • 返回行数不大

反过来,如果是下面这些情况,我不会先指望它:

  • 每次参数都不同
  • 结果集很大
  • 数据变化非常频繁且必须实时可见
  • 真正瓶颈在表结构或过滤条件

可以继续延伸看的主题

参考资料

备注

本文引用的官方文章发布时间为 2023-02-09,当时 Query Cache 在 v23.1 中以实验特性引入。
如果你在较新的 ClickHouse 版本上使用,建议再对照当前官方文档核对具体配置项和默认行为。