参考ClickHouse官方文档

https://clickhouse.com/blog/introduction-to-the-clickhouse-query-cache-and-design

问题:初步检查,目前生产环境的缓存未生效

可能存在的问题1 — 缓存大小超过了Query Cache的上限

默认情况下(当前配置),单条缓存存在两个限制:

  1. 单条结果字节上限 (max_entry_size_in_bytes):1MB

  2. 单条结果行数上限 (max_entry_size_in_rows):30_000_000

我在生产环境grafna中检查:

确实存在超过1MB的查询结果(1.60/1.20 MiB),但是数量较少,占比约1/100

rows最大也就16,可以排除。

因此目前判断:

  • max_entry_size_in_bytes = 1MB 可能是部分查询无法进入缓存的原因

  • 但从比例看,它不像是“缓存整体未生效”的唯一主因

可能存在的问题2 — 查询走不同副本导致cache miss

目前生产环境确实是多副本

排查思路

生产环境是否命中 query cache,优先从三个位置排查:

  1. system.query_log 中的 ProfileEvents['QueryCacheHits']

  2. system.query_cache

  3. send_logs_level='trace' 下的 ClickHouse 服务端日志

  4. 两次相同 SQL 是否落在同一个副本上(当前的多副本情况)

1. 最小验证

在历史记录中,选一条固定的耗时SQL(当前配置是大于7s)(这里以media SQL为例),连续执行两次,要求:

  • SQL 文本完全一致(复制粘贴同一条SQL)

  • 两次执行间隔小于 query_cache_ttl(目前是10s)

  • 两次都带相同的 SETTINGS

执行后查询(根据SQL情况需要调整%FROM app.media 的参数,后面不再赘述):

SELECT
  query_id,
  query_duration_ms,
  ProfileEvents['QueryCacheHits'] AS cache_hits,
  query
FROM system.query_log
WHERE type = 'QueryFinish'
  AND query LIKE '%FROM app.media%'
ORDER BY event_time DESC
LIMIT 5;

预期:

  • 第一次:cache_hits = 0

  • 第二次:cache_hits > 0

如果第二次仍然是 0,继续往下查。

2. 检查是否成功写入缓存

SELECT *
FROM system.query_cache
ORDER BY expires_at DESC
LIMIT 20;

情况 A:system.query_cache 为空

说明问题不在“命中失败”,而在“第一次根本没有写入缓存”。

此时重点排查:

  • 查询结果是否超过 1MB

  • SQL 是否包含导致缓存被跳过的因素

  • ClickHouse 是否明确记录了跳过写入的原因

可以打开 trace 日志后重跑:

SET send_logs_level = 'trace';

重点查看是否出现类似日志:

  • Skipped insert (query result too big)

  • No entry found for query ...

官方文档中也是通过 trace 日志定位“结果过大导致未写入缓存”的。

情况 B:system.query_cache 有条目,但第二次仍未命中

说明缓存已经写入,但第二次查询没有复用到。

这时最优先检查:两次 SQL 是否真的完全一致

3. 检查两次 SQL 文本是否完全一致

query cache 依赖的是查询文本,而不是逻辑等价。

重点检查:

  • projectId 段落顺序是否一致

  • (experimentId, key) IN (...) 内部 tuple 顺序是否一致

  • SETTINGS 是否完全一致

  • 第二次执行时是否已经超过 query_cache_ttl

可以直接从 system.query_log 中对比查询文本:

SELECT
  event_time,
  query_id,
  query
FROM system.query_log
WHERE type = 'QueryFinish'
  AND query LIKE '%FROM app.media%'
ORDER BY event_time DESC
LIMIT 5;

如果两次 query 字符串不同,即使语义相同,也不会命中。

4. 检查是否存在多副本切换

如果生产环境是多副本部署,query cache 很可能是节点本地缓存,不是跨副本共享的。(目前查到的资料显示ClickHouse不支持分布式缓存)

这种情况下:

  • 第一次查询落在副本 A

  • 第二次相同查询落在副本 B

即使 SQL 文本完全一致,也可能不会命中缓存。

建议从 system.query_log 中查看两次查询的执行节点信息,例如:

  • hostname

  • address

  • initial_address

  • replica

如果能确认两次查询落在不同副本,那么“多副本切换导致 miss”就是非常高优先级的原因。

进一步可以做一个最小验证:

-- 将 your_cluster 替换为生产环境实际的 ClickHouse cluster 名称
-- first_query_id和second_query_id用对应的ID
SELECT
    hostName() AS host,
    event_time,
    query_id,
    initial_query_id,
    query_duration_ms,
    ProfileEvents['QueryCacheHits'] AS cache_hits,
    query
FROM clusterAllReplicas('your_cluster', system.query_log)
WHERE type = 'QueryFinish'
  AND query_id IN (
    'first_query_id',
    'second_query_id'
  )
ORDER BY event_time;

如果两次查询的 host 不同,同时第二次 cache_hits = 0,那么“多副本切换导致缓存未复用”就是非常强的候选原因。

5. 检查 query cache 的前置 setting

需要确认生产上实际执行的 SQL 是否带有完整的 query cache setting:

  • allow_experimental_query_cache = 1

  • use_query_cache = true

  • query_cache_ttl = 10

  • query_cache_min_query_duration = 7000

  • query_cache_nondeterministic_function_handling = 'save'

如果某次实际执行的 SQL 缺少这些 setting,或者值不一致,都会影响缓存行为。

当前优先级最高的几个原因

按优先级排序,当前更可能的是:

  1. 部分查询结果超过 1MB,首次就没有写入缓存

  2. 多副本导致落在不同副本导致的cache miss

  3. 第二次查询已经超过 query_cache_ttl

  4. 实际执行 SQL 没带完整 query cache settings