1. 查询缓存的价值
OLAP 查询常重复执行:仪表盘每小时刷新同一 SQL、监控面板轮询同一聚合。每次都全量扫描既浪费 CPU/IO,也拉长响应。
无缓存:每条重复查询都重算 → 慢 & 费资源
有缓存:命中直接返回结果 → 快 & 省资源
预热:把"高成本查询"提前算好 → 冷启动变热启动
一句话总结:缓存的本质是"用内存换重复计算"——对固定模式的高频查询,一次计算、多次复用。
2. 查询结果缓存(Query Cache)
ClickHouse 从 v23.x 起提供官方 Query Cache,缓存查询结果集。
2.1 启用与配置
<clickhouse>
<query_cache>
<max_size_in_bytes>104857600</max_size_in_bytes> <!-- 100MB -->
<max_entries>1024</max_entries>
<max_entry_size_in_bytes>1048576</max_entry_size_in_bytes>
<max_entry_size_in_rows>30000000</max_entry_size_in_rows>
</query_cache>
</clickhouse>
2.2 使用
-- 强制使用缓存
SELECT sum(revenue) FROM sales SETTINGS use_query_cache = 1;
-- 带缓存条件:结果小、确定性查询
SELECT count() FROM events SETTINGS use_query_cache = 1;
2.3 什么查询适合缓存
| 适合 | 不适合 |
|---|---|
| 固定 SQL 的仪表盘 | 结果超大 |
| 低频变化的数据 | 涉及随机函数 |
| 小结果集聚合 | 依赖系统时间 |
| 高重复率 | 实时性要求毫秒级 |
2.4 缓存的限制
- 结果需确定性(无随机、无 currentDate 差异敏感)
- 大结果集不缓存(超过 max_entry_size 直接旁路)
- 缓存默认按查询文本精确匹配
一句话总结:Query Cache 适合"结果小、重复多、数据低频变化"的查询;它替代不了物化视图,但填补了"临时结果复用"的空档。
3. 分析查询成本:profile events
优化前先量化:哪些查询最贵、贵在哪。
-- 查看查询事件统计
SELECT
query,
read_rows,
read_bytes,
memory_usage,
query_duration_ms,
ProfileEvents['QueryCacheHits'] AS cache_hits,
ProfileEvents['QueryCacheMisses'] AS cache_misses
FROM system.query_log
ORDER BY query_duration_ms DESC
LIMIT 20;
3.1 关键 profile events
| 事件 | 含义 |
|---|---|
SelectedRows | 扫描行数(决定耗时) |
SelectedBytes | 扫描字节数 |
QueryCacheHits/Misses | 缓存命中/未命中 |
CachedReadBufferReadBytes | 页缓存命中 |
MergeTreeDataReadRows | 实际读出的行 |
3.2 找热点查询
-- 重复度最高的查询(可作为缓存/预热候选)
SELECT
normalizeQuery(query) AS q,
count() AS cnt,
avg(query_duration_ms) AS avg_ms
FROM system.query_log
WHERE event_date = today() AND type = 'QueryFinish'
GROUP BY q
ORDER BY cnt DESC
LIMIT 20;
一句话总结:调优的第一步是用 query_log 找到"重复多、耗时长"的查询——它们是缓存与预热最值得投资的对象。
4. 预热手段:物化视图与预聚合
4.1 物化视图 = 终极预热
对"固定聚合 + 高频查询"的场景,物化视图把聚合结果增量预存,查询直接读聚合表:
CREATE MATERIALIZED VIEW mv_daily_sales
ENGINE = AggregatingMergeTree()
ORDER BY (day, product)
AS SELECT
toDate(ts) AS day,
product,
sumState(revenue) AS revenue_sum
FROM sales
GROUP BY day, product;
4.2 查询
-- 直接查物化视图,秒回(无需扫原始表)
SELECT day, product, sumMerge(revenue_sum)
FROM mv_daily_sales
WHERE day = today() AND product = 'apple';
4.3 预热策略对比
| 手段 | 粒度 | 更新方式 | 适合 |
|---|---|---|---|
| Query Cache | 查询结果 | 自动(LRU) | 临时重复查询 |
| 物化视图 | 聚合结果 | 增量实时 | 固定聚合模式 |
| 预聚合宽表 | 明细 | ETL 刷新 | 多维分析 |
| 分区预计算 | 分区级 | 分区 TTL | 时间维度 |
一句话总结:物化视图把"查询时计算"变成"写入时计算",是结构性最强的预热——高频固定聚合应当优先考虑它。
5. 热点查询治理
5.1 治理流程
1. 识别热点:query_log 找高重复 + 高耗时
2. 分类:
- 固定聚合 → 物化视图
- 临时重复 → Query Cache
- 可下沉 → 预计算宽表
3. 执行改造 + 对比延迟
4. 纳入监控:跟踪热点的耗时回归
5.2 报表场景实践
-- 报表原查询(每次全扫)
SELECT
toDate(ts) AS day,
countDistinct(user_id) AS dau
FROM events
WHERE ts >= today() - 7
GROUP BY day;
-- 治理:建物化视图 + 查询走聚合表
CREATE MATERIALIZED VIEW mv_dau
ENGINE = AggregatingMergeTree()
ORDER BY day
AS SELECT
toDate(ts) AS day,
uniqState(user_id) AS dau_state
FROM events
GROUP BY day;
SELECT day, uniqMerge(dau_state) AS dau FROM mv_dau;
5.3 缓存命中率的监控
-- 缓存命中率
SELECT
countIf(ProfileEvents['QueryCacheHits'] > 0) AS hits,
countIf(ProfileEvents['QueryCacheMisses'] > 0) AS misses,
hits / (hits + misses) AS hit_rate
FROM system.query_log
WHERE event_date = today();
一句话总结:热点治理不是"加个缓存"就完事,而是"识别 → 分类 → 改造 → 监控"的闭环;物化视图承接结构性热点,Query Cache 承接临时热点。
6. 缓存失效与实时性权衡
6.1 新鲜度问题
Query Cache 缓存的是"某个时刻的结果"
→ 新数据写入后,缓存可能过期
→ 需要明确:实时性 vs 缓存收益
取舍策略:
高频报表:可接受 1-5 分钟延迟 → 用缓存
实时监控:毫秒级新鲜 → 慎用缓存
折中:短 TTL + 分区级失效
6.2 失效策略
| 策略 | 机制 |
|---|---|
| 时间过期 | 按 LIFETIME/TTL 定期失效 |
| 手动清空 | SYSTEM DROP QUERY CACHE |
| 分区级失效 | 新分区写入触发相关缓存失效 |
| 查询跳过 | 特定查询关闭缓存 |
-- 手动清空缓存
SYSTEM DROP QUERY CACHE;
-- 单条查询不用缓存
SELECT count() FROM events SETTINGS use_query_cache = 0;
6.3 决策表
| 数据变化 | 查询重复 | 实时要求 | 建议 |
|---|---|---|---|
| 低频 | 高 | 宽松 | Query Cache + 物化视图 |
| 高频 | 高 | 严格 | 物化视图(增量实时) |
| 低频 | 低 | 宽松 | 不用缓存 |
| 高频 | 低 | 严格 | 直接查,优化索引 |
一句话总结:缓存与实时性是同一枚硬币的两面——用"数据变化率 × 查询重复度 × 实时要求"三维决策,而不是无脑加缓存。
7. 其他查询加速手段
7.1 Prepared Statements
PREPARE my_q AS
SELECT * FROM events WHERE user_id = {id: UInt64};
EXECUTE my_q USING id = 42;
-- 避免每次解析 SQL,降低解析开销
7.2 分片并行与集群缓存
分布式查询:GLOBAL IN / GLOBAL JOIN 缓存子查询结果
减少跨分片重复拉取
7.3 分区修剪
-- 让查询只扫需要的分区
SELECT * FROM events
WHERE ts >= '2026-09-01' AND ts < '2026-09-29';
-- 配合分区键,物理跳过无关分区
8. 生产实践清单
| 主题 | 核心结论 |
|---|---|
| Query Cache | 适合小结果、重复多、变化慢的查询 |
| 成本分析 | query_log + profile events 定位热点 |
| 结构性预热 | 物化视图增量预聚合,固定聚合首选 |
| 热点治理 | 识别 → 分类 → 改造 → 监控闭环 |
| 失效权衡 | 数据变化率 × 重复度 × 实时性决策 |
| 辅助手段 | Prepared、GLOBAL 缓存、分区修剪 |
缓存不是银弹,而是一套分层体系:物化视图做结构性预热、Query Cache 做临时复用、分区修剪做物理剪枝。把每一层用在合适的位置,热点查询才能真正"秒回"。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。