SELECT 查询只计算一次,后续相同查询的执行结果可直接从缓存中返回。
根据查询类型的不同,这可以显著降低 ClickHouse 服务器的延迟和资源消耗。
背景、设计与局限性
- 在事务一致的缓存中,如果
SELECT查询的结果已经变化 或可能发生变化,数据库就会使已缓存的查询结果失效 (丢弃) 。在 ClickHouse 中,会更改数据的操作包括对表执行 insert/update/delete,或发生 collapsing merges。事务一致的缓存尤其适用于 OLTP 数据库,例如 MySQL (已在 v8.0 之后移除 query cache) 以及 Oracle。 - 在事务不一致的缓存中,可以接受查询结果存在轻微误差,前提是所有缓存条目
都会被设置一个有效期,过期后即失效 (例如 1 分钟) ,并且底层数据在这段时间内变化很小。
这种方式整体上更适合 OLAP 数据库。一个事务不一致缓存已足够的例子是:
报表工具中的每小时销售报表被多个用户同时访问。销售数据的变化通常
足够缓慢,因此数据库只需计算一次报表 (即第一次
SELECT查询) 。后续查询可以 直接由查询缓存返回结果。在这个例子中,合理的有效期可以是 30 分钟。
配置设置与用法
clickhouse-local 一次只能运行一条查询。对单个进程而言,在内存中缓存查询结果并无实际意义,
因此 clickhouse-local 中禁用了内存查询缓存。不过,磁盘上的查询缓存仍可使用:其条目存放在磁盘上的
文件系统缓存中,因此某个 clickhouse-local 进程缓存的结果,可供之后使用同一文件系统缓存目录的其他进程直接复用。
use_query_cache = true) ,将会
直接从缓存中读取已计算的结果并立即返回。
use_query_cache 以及所有其他与查询缓存相关的设置,只有对独立的 SELECT 语句才会生效。特别是,
对通过 CREATE VIEW AS SELECT [...] SETTINGS use_query_cache = true 创建的视图执行 SELECT 时,其结果不会被缓存,除非该 SELECT
语句在运行时指定了 SETTINGS use_query_cache = true。true) 更细致地配置缓存的使用方式。前者
控制是否将查询结果写入缓存,而后者则决定数据库是否尝试从缓存中读取查询
结果。例如,下面这个查询只会被动使用缓存,也就是说,它会尝试从中读取,但不会将其
结果写入缓存:
use_query_cache、enable_writes_to_query_cache 和
enable_reads_from_query_cache。也可以在用户或 profile 级别启用缓存 (例如通过 SET use_query_cache = true) ,但需要注意的是,这样一来,所有 SELECT 查询之后都可能返回缓存结果。
可以使用语句 SYSTEM CLEAR QUERY CACHE 清空查询缓存。查询缓存的内容显示在系统表
system.query_cache 中。自数据库启动以来的查询缓存命中和未命中次数,会作为事件
“QueryCacheHits” 和 “QueryCacheMisses” 显示在系统表 system.events 中。这两个计数器只会对
在设置 use_query_cache = true 下运行的 SELECT 查询进行更新,其他查询不会影响 “QueryCacheMisses”。它们反映的是整个查询
缓存的情况:启用磁盘查询缓存 (见下文) 时,在内存中未命中、随后由磁盘提供结果的查询计为
一次 “QueryCacheHits”,而在两个后端中均未命中的查询则计为一次 “QueryCacheMisses”。系统表
system.query_log 中的字段 query_cache_usage 会显示每个已执行查询的结果是否被写入
查询缓存,或者是否从查询缓存中读取。系统表中的指标 QueryCacheEntries 和 QueryCacheBytes
system.metrics 显示内存查询缓存当前包含多少条目 / 字节。它们
不统计磁盘查询缓存中的条目,磁盘查询缓存目前没有占用量指标。
每个 ClickHouse server process 都有一个查询缓存实例。不过,默认情况下,缓存结果不会在用户之间共享。这一点可以
更改 (见下文) ,但出于安全原因,不建议这样做。
查询结果在查询缓存中以其查询的抽象语法树 (AST) 作为引用依据。
这意味着缓存不区分大小写,例如 SELECT 1 和 select 1 会被视为同一个查询。为了
使匹配更加自然,所有与查询缓存和输出格式化)相关的查询级别设置
都会从 AST 中移除。
如果查询因异常或用户取消而中止,则不会向查询缓存写入任何条目。
查询缓存的大小 (以字节为单位) 、缓存条目的最大数量以及单个缓存条目的最大大小 (以字节和
记录数计) 均可通过相应的服务器配置选项进行配置。
users.xml 的用户 SETTINGS PROFILE 中配置
query_cache_max_size_in_bytes 和
query_cache_max_entries,然后将这两个设置都设为
只读:
Age 和 Expires 请求头,其中包含该
缓存条目的缓存时长 (以秒为单位) 和过期时间戳。
查询缓存条目默认会被压缩。这样可以减少总体内存占用,但代价是写入查询缓存 / 从查询缓存读取
的速度会变慢。要禁用压缩,请使用设置 query_cache_compress_entries。
有时,让同一查询的多个结果同时保留在缓存中会很有用。这可以通过设置
query_cache_tag 来实现,它充当查询缓存条目的标签 (或命名空间) 。查询缓存
会将带有不同标签的同一查询结果视为不同结果。
为同一查询创建三个不同查询缓存条目的示例:
tag 的条目,可以使用语句 SYSTEM CLEAR QUERY CACHE TAG 'tag'。
子查询缓存
默认情况下,外层查询上的use_query_cache 设置不会传递给子查询。这意味着每个子查询都必须显式启用缓存:
query_cache_for_subqueries:
use_query_cache = false:
is_subquery = 1。如果启用了磁盘查询缓存,子查询结果也会缓存到磁盘上,但这些条目不会显示在 system.query_cache 中。query_cache_ttl 设置也适用于子查询缓存条目,并且可以为每个子查询单独设置。
ClickHouse 按 max_block_size 行一块读取表数据。由于过滤、aggregation
等操作,结果块通常会比 ‘max_block_size’ 小得多,但在某些情况下也可能大得多。设置
query_cache_squash_partial_results (默认启用) 用于控制结果块
在插入查询结果
缓存 之前,是否会被压缩合并 (如果块很小) 或拆分 (如果块很大) 为大小为 ‘max_block_size’ 的块。这会降低写入 查询缓存 的性能,但能提高缓存条目的压缩率,并在之后从 查询缓存 返回查询结果时提供更合理的
块粒度。
因此,对于每个查询,查询缓存 都会存储多个 (部分)
结果块。虽然这种行为作为默认设置是合理的,但也可以通过设置
query_cache_squash_partial_results 关闭。
此外,包含非确定性函数的查询结果默认不会被缓存。这类函数包括
- 用于访问字典的函数:
dictGet()等。 - XML
定义中未包含标签
<deterministic>true</deterministic>的用户自定义函数, - 返回当前日期或时间的函数:
now(),today(),yesterday()等, - 返回随机值的函数:
randomString(),fuzzBits()等, - 结果取决于查询处理过程中所用内部 chunks 的大小和顺序的函数:
nowInBlock()等,rowNumberInBlock(),runningDifference(),blockSize()等, - 依赖环境的函数:
currentUser(),queryID(),getMacro()等。
磁盘上的查询缓存
默认情况下,查询缓存将其条目保存在内存中。此外,查询结果也可以缓存到磁盘上,并由 文件系统缓存提供存储支持。与内存中的查询缓存相比,磁盘上的 查询缓存空间更大,且在服务器重启后依然保留。条目只能由写入它的同一服务器构建版本读取: 升级或降级后,先前构建版本写入的条目均视为缓存未命中,并最终被覆盖或淘汰。 要使用磁盘上的查询缓存,需先在服务器配置的filesystem_caches 部分中配置文件系统缓存:
query_cache_on_disk_cache_name 中指定该缓存的名称:
enable_reads_from_query_cache_on_disk 和 enable_writes_to_query_cache_on_disk (二者默认均为 true
) 分别启用读取和/或写入。如果两种缓存都启用了读取,则会先在内存中查找,仅在未命中时才查找磁盘。如果两种缓存
都启用了写入,则结果会同时存储到两者中。如果没有任何缓存可以存储结果 (例如在 clickhouse-local 中,它没有
内存查询缓存,且 enable_writes_to_query_cache_on_disk = false) ,查询不会因为包含非确定性
函数、系统表或非 throw 溢出模式而被拒绝:这些检查仅用于防止存储错误的结果。磁盘上的条目使用设置
query_cache_on_disk_codec 指定的编解码器进行压缩 (默认为 ZSTD(3)) 。查询缓存的服务器设置 max_entry_size_in_bytes 和 max_entry_size_in_rows
同时限制两种后端的条目;每种后端都按照自身的存储方式来衡量条目大小,因此在磁盘上,字节限制针对的是
序列化并压缩后的条目。将其中任一设置设为 0 会禁止向两种后端写入。除下述两个例外,所有
其他查询缓存设置 (例如 query_cache_ttl、query_cache_min_query_duration) 对磁盘查询缓存的作用方式均与
对内存查询缓存相同。
设置 query_cache_max_size_in_bytes 和 query_cache_max_entries 仅是内存查询缓存的按用户限制,
对磁盘查询缓存不生效:磁盘上的条目受所配置的文件系统缓存大小限制,并按其规则淘汰
(见下文) ,因此即使用户的 SETTINGS PROFILE 限制较严,其在磁盘上存储的结果仍可能超过这些设置在
内存中允许的数量。这是有意设计的——文件系统缓存是一种共享且有大小上限的资源,磁盘查询缓存不会在其中预留
空间。
设置 query_cache_share_between_users 是另一个例外:它对两种后端的含义相同——启用该设置写入的条目会提供给
所有用户——但非共享条目的隔离方式不同。内存查询缓存对每个查询只保留一个条目,读取时若发现该条目属于其他用户则予以拒绝,
因此用户 A 的非共享条目会遮蔽用户 B 的写入,直到该条目过期或被
淘汰。而磁盘查询缓存则将用户及其当前启用的角色集合纳入非共享条目的键中,因此
用户 B 不会命中用户 A 的条目,而是存储自己的一份结果副本。同一用户在不同的当前启用的角色集合下也是如此,
例如执行 SET ROLE 之后:角色附带特权和 ROW POLICY,因此在不同角色下计算出的结果会分别
缓存。
磁盘上的查询缓存条目是文件系统缓存中的普通条目,与同一
文件系统缓存中缓存的其他数据并无区别:它们不受删除保护,当文件系统缓存空间不足时,也会按照相同的规则 (例如最近最少使用) 被淘汰。
它们的总大小或数量没有单独的限制——只受文件系统
缓存本身的限制约束。语句 SYSTEM CLEAR QUERY CACHE (以及 SYSTEM CLEAR QUERY CACHE TAG 'tag') 还会从会话设置 query_cache_on_disk_cache_name 所选定的文件系统缓存中
(例如在执行 SET query_cache_on_disk_cache_name = 'query_results' 之后) 删除磁盘上的查询缓存条目 (对于后者,仅删除带有
指定标签的条目;但损坏的条目以及格式不兼容的条目无论标签如何都会被删除) ,
同时保留文件系统缓存中的其他数据。为了
找到这些条目,该语句会遍历文件系统缓存中所有 File 段的元数据 (在内存中进行,无需读取磁盘) ,并读取
每个被标记为查询缓存条目的 key 的请求头。遍历耗时与文件系统缓存中 File 段的数量成正比,
因此为查询结果使用专用的文件系统缓存,可以让 SYSTEM CLEAR QUERY CACHE 保持低开销。如果
文件系统缓存已经淘汰了某个条目的开头部分,则该条目的剩余部分会保留在原处,直到被文件系统缓存
淘汰或同一查询结果被重新写入,但它永远不会被用来返回结果。仍在写入中的
条目同样会被保留。语句 SYSTEM DROP FILESYSTEM CACHE '<name>' 则会将这些条目连同
文件系统缓存中的其他所有内容一并删除。系统表
system.query_cache 以及指标 QueryCacheEntries 和 QueryCacheBytes 仅显示
内存查询缓存中的条目。磁盘查询
缓存的命中数和未命中数分别以事件 “QueryCacheOnDiskHits” 和 “QueryCacheOnDiskMisses” 显示在系统表 system.events 中。
这两个事件仅是磁盘后端的细分统计,而 “QueryCacheHits” 和
“QueryCacheMisses” 仍是整个查询缓存的计数器,“QueryCacheAgeSeconds” 则同时统计两种后端的命中。由于
系统会先探测内存缓存,因此内存命中数等于 “QueryCacheHits” 减去 “QueryCacheOnDiskHits”。