Skip to main content
查询缓存允许 SELECT 查询只计算一次,后续相同查询的执行结果可直接从缓存中返回。 根据查询类型的不同,这可以显著降低 ClickHouse 服务器的延迟和资源消耗。

背景、设计与局限性

查询缓存通常可分为事务一致和事务不一致两类。
  • 在事务一致的缓存中,如果 SELECT 查询的结果已经变化 或可能发生变化,数据库就会使已缓存的查询结果失效 (丢弃) 。在 ClickHouse 中,会更改数据的操作包括对表执行 insert/update/delete,或发生 collapsing merges。事务一致的缓存尤其适用于 OLTP 数据库,例如 MySQL (已在 v8.0 之后移除 query cache) 以及 Oracle。
  • 在事务不一致的缓存中,可以接受查询结果存在轻微误差,前提是所有缓存条目 都会被设置一个有效期,过期后即失效 (例如 1 分钟) ,并且底层数据在这段时间内变化很小。 这种方式整体上更适合 OLAP 数据库。一个事务不一致缓存已足够的例子是: 报表工具中的每小时销售报表被多个用户同时访问。销售数据的变化通常 足够缓慢,因此数据库只需计算一次报表 (即第一次 SELECT 查询) 。后续查询可以 直接由查询缓存返回结果。在这个例子中,合理的有效期可以是 30 分钟。
传统上,事务不一致的缓存通常由与数据库交互的客户端工具或代理软件包提供 (例如 chproxy) 。因此,相同的缓存逻辑和 配置往往会被重复实现。借助 ClickHouse 的查询缓存,缓存逻辑被移到了服务端。这减少了维护 工作量,也避免了重复。

配置设置与用法

在 ClickHouse Cloud 中,必须使用查询级别设置来修改查询缓存设置。目前暂不支持修改配置级别设置。
clickhouse-local 一次只能运行一条查询。对单个进程而言,在内存中缓存查询结果并无实际意义, 因此 clickhouse-local 中禁用了内存查询缓存。不过,磁盘上的查询缓存仍可使用:其条目存放在磁盘上的 文件系统缓存中,因此某个 clickhouse-local 进程缓存的结果,可供之后使用同一文件系统缓存目录的其他进程直接复用。
可通过设置 use_query_cache 来控制某个特定查询或当前会话中的所有查询 是否使用查询缓存。例如,首次执行该查询时
会将查询结果存入查询缓存。之后再次执行相同的查询时 (同样带有参数 use_query_cache = true) ,将会 直接从缓存中读取已计算的结果并立即返回。
use_query_cache 以及所有其他与查询缓存相关的设置,只有对独立的 SELECT 语句才会生效。特别是, 对通过 CREATE VIEW AS SELECT [...] SETTINGS use_query_cache = true 创建的视图执行 SELECT 时,其结果不会被缓存,除非该 SELECT 语句在运行时指定了 SETTINGS use_query_cache = true。
还可以通过设置 enable_writes_to_query_cache 和 enable_reads_from_query_cache (两者默认值均为 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 中移除。 如果查询因异常或用户取消而中止,则不会向查询缓存写入任何条目。 查询缓存的大小 (以字节为单位) 、缓存条目的最大数量以及单个缓存条目的最大大小 (以字节和 记录数计) 均可通过相应的服务器配置选项进行配置。
也可以使用 SETTINGS PROFILE 和 设置约束 来限制单个用户的缓存使用量。更具体地说,你可以限制用户在内存查询缓存中可 分配的最大内存量 (以字节为单位) ,以及可存储的查询结果的最大数量 (这些限制不适用于磁盘上的查询 缓存) 。为此,首先在 users.xml 的用户 SETTINGS PROFILE 中配置 query_cache_max_size_in_bytes 和 query_cache_max_entries,然后将这两个设置都设为 只读:
要指定查询至少运行多长时间,其结果才可被缓存,可以使用设置 query_cache_min_query_duration。例如,对于查询
仅当查询运行时间超过 5 秒时,其结果才会被缓存。还可以指定查询需要运行多少次后,其结果才会被 缓存——为此请使用设置 query_cache_min_query_runs。 查询缓存条目会在一定时间后变为过期 (生存时间 (TTL)) 。默认情况下,这个时间为 60 秒,但也可以在会话、SETTINGS PROFILE 或查询级别 使用设置 query_cache_ttl 指定其他值。查询 缓存会”惰性”淘汰条目,也就是说,当某个条目过期后,它不会立即从缓存中移除。相反,当一个新条目 准备插入查询缓存时,数据库会检查缓存是否有足够的可用空间容纳该新条目。如果 没有,数据库会尝试移除所有已过期的条目。如果查询缓存仍然没有足够的可用空间,则不会插入新条目。 如果查询是通过 HTTP 运行的,那么 ClickHouse 会设置 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:
内存中查询缓存的子查询缓存条目在 system.query_cache 中可见,其中 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 关闭。 此外,包含非确定性函数的查询结果默认不会被缓存。这类函数包括 如果无论如何都要强制缓存包含非确定性函数的查询结果,请使用设置 query_cache_nondeterministic_function_handling。 默认情况下,涉及系统表 (例如 system.processes` 或 information_schema.tables) 的查询结果不会被缓存。如需强制缓存涉及 系统表的查询结果,请使用设置 query_cache_system_table_handling。 最后,出于安全原因,查询缓存 中的条目不会在用户之间共享。例如,用户 A 不应通过执行与另一位用户 B 相同的查询来绕过 表上的 ROW POLICY,因为用户 B 可能并不存在这样的策略。不过,如有需要,可以通过提供设置 query_cache_share_between_users,将缓存条目标记为其他用户也可访问 (即共享) 。

磁盘上的查询缓存

默认情况下,查询缓存将其条目保存在内存中。此外,查询结果也可以缓存到磁盘上,并由 文件系统缓存提供存储支持。与内存中的查询缓存相比,磁盘上的 查询缓存空间更大,且在服务器重启后依然保留。条目只能由写入它的同一服务器构建版本读取: 升级或降级后,先前构建版本写入的条目均视为缓存未命中,并最终被覆盖或淘汰。 要使用磁盘上的查询缓存,需先在服务器配置的 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”。