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 里,性能优化大致可以拆成三层:
- 表设计是否合理
- SQL 是否减少了不必要的扫描和计算
- 重复查询能否被缓存复用
Query Cache 主要解决第三层。
所以更合理的使用顺序通常是:
- 先把 SQL 本身写对
- 再判断该查询是否高频重复
- 最后再看是否值得使用 Query Cache
我的实践判断
如果一个查询同时满足下面几点,我会优先考虑它:
- 查询本身不短
- 会被固定面板反复执行
- 结果允许几十秒到几分钟的陈旧
- 返回行数不大
反过来,如果是下面这些情况,我不会先指望它:
- 每次参数都不同
- 结果集很大
- 数据变化非常频繁且必须实时可见
- 真正瓶颈在表结构或过滤条件
可以继续延伸看的主题
- ClickHouse 基本介绍
- MergeTree
- 排序键
- 物化视图
- 预聚合
- Query Condition Cache
参考资料
- ClickHouse 官方博客:Introducing the ClickHouse Query Cache
https://clickhouse.com/blog/introduction-to-the-clickhouse-query-cache-and-design - ClickHouse 官方文档首页
https://clickhouse.com/docs
备注
本文引用的官方文章发布时间为 2023-02-09,当时 Query Cache 在 v23.1 中以实验特性引入。
如果你在较新的 ClickHouse 版本上使用,建议再对照当前官方文档核对具体配置项和默认行为。