ClickHouse系列(七):Materialized View 与多分辨率 Rollup 设计

聚合与趋势------分钟到月级趋势如何高效查询

引言

监控系统每秒写入百万条指标,但用户看的是"过去 7 天的 QPS 趋势"。如果每次查询都扫描原始数据,集群迟早扛不住。ClickHouse 的 Materialized View(MV)+ AggregatingMergeTree 提供了一套优雅的多级 Rollup 方案:写入时自动聚合,查询时直接读预计算结果。

本篇将深入 MV 的执行机制、性能代价,以及如何设计 Raw → 10min → 1h → 1d 的多分辨率聚合链路。


1. MV 是同步计算,不是后台任务

很多人第一次接触 MV 时会以为它类似 ETL 定时任务。事实恰好相反:

复制代码
INSERT 原始数据 → 触发 MV 的 SELECT → 结果写入目标表

这三步在同一次 INSERT 的执行路径中同步完成。这意味着:

  • MV 的 SELECT 越复杂,INSERT 延迟越高
  • MV 数量越多,每次写入的扇出越大
  • 如果目标表写入失败,原始表的 INSERT 也会失败

验证方式------查看 system.query_log,你会发现每次 INSERT 后紧跟着 MV 触发的子查询:

sql 复制代码
SELECT query, type, query_kind
FROM system.query_log
WHERE query LIKE '%mv_target_table%'
ORDER BY event_time DESC
LIMIT 5;

核心认知:MV 不是"免费午餐",它用写入时的 CPU 换取查询时的 IO。


2. MV 的性能成本在哪里

成本维度 说明
CPU MV 的 SELECT 在 INSERT 线程中执行,GROUP BY、函数计算都消耗 CPU
内存 聚合状态(AggregateFunction)需要在内存中维护 hash table
写放大 1 条 INSERT 触发 N 个 MV,磁盘写入量翻 N 倍
延迟 INSERT 的 latency = 原始写入 + 所有 MV 执行时间之和
Merge 压力 目标表产生大量小 part,后台 merge 线程压力增大

实际经验值:

  • 3 个简单 MV(SUM/COUNT)对写入延迟的影响约 15-30%
  • 超过 5 个 MV 时建议评估是否拆分写入链路
  • MV 的 SELECT 中避免 JOIN,否则写入性能会断崖式下降

3. Raw / 10min / 1h 表的职责划分

多分辨率 Rollup 的核心思想是:不同时间粒度的查询,命中不同的表

粒度 TTL 典型查询场景
metrics_raw 原始(秒级) 3 天 实时告警、最近 1 小时明细
metrics_10min 10 分钟 30 天 过去 24 小时趋势图
metrics_1h 1 小时 180 天 周报、月度趋势
metrics_1d 1 天 3 年 年度同比、容量规划

原始表定义:

sql 复制代码
CREATE TABLE metrics_raw (
    ts DateTime,
    host String,
    metric String,
    value Float64
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(ts)
ORDER BY (metric, host, ts)
TTL ts + INTERVAL 3 DAY;

关键设计原则:

  • 原始表保留最短时间,用 TTL 自动清理
  • 聚合表保留更长时间,数据量小得多
  • 查询层根据时间范围自动路由到对应的表

4. AggregatingMergeTree + MV 的正确组合

AggregatingMergeTree 的核心能力是:在后台 merge 时继续聚合。配合 MV,实现"写入时预聚合 + merge 时再聚合"的两阶段计算。

第一步:创建目标表

sql 复制代码
CREATE TABLE metrics_10min (
    ts_bucket DateTime,
    host String,
    metric String,
    value_sum AggregateFunction(sum, Float64),
    value_max AggregateFunction(max, Float64),
    value_count AggregateFunction(count, Float64)
) ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMMDD(ts_bucket)
ORDER BY (metric, host, ts_bucket)
TTL ts_bucket + INTERVAL 30 DAY;

第二步:创建 MV

sql 复制代码
CREATE MATERIALIZED VIEW mv_metrics_10min
TO metrics_10min
AS SELECT
    toStartOfTenMinutes(ts) AS ts_bucket,
    host,
    metric,
    sumState(value)   AS value_sum,
    maxState(value)   AS value_max,
    countState(value)  AS value_count
FROM metrics_raw
GROUP BY ts_bucket, host, metric;

第三步:查询时用 Merge 函数

sql 复制代码
SELECT
    ts_bucket,
    host,
    sumMerge(value_sum)   AS total,
    maxMerge(value_max)   AS peak,
    countMerge(value_count) AS cnt
FROM metrics_10min
WHERE metric = 'cpu_usage'
  AND ts_bucket >= now() - INTERVAL 24 HOUR
GROUP BY ts_bucket, host
ORDER BY ts_bucket;

注意 sumState / sumMerge 的配对关系:

阶段 函数 作用
MV 写入 sumState(value) 将值编码为中间聚合状态
查询 sumMerge(value_sum) 将多个中间状态合并为最终结果

如果直接用 sum(value_sum) 查询 AggregatingMergeTree,会得到错误结果或报错。


5. 多级 Rollup 的写入与查询策略

链式 MV:从 10min 再聚合到 1h

sql 复制代码
CREATE TABLE metrics_1h (
    ts_bucket DateTime,
    host String,
    metric String,
    value_sum AggregateFunction(sum, Float64),
    value_max AggregateFunction(max, Float64),
    value_count AggregateFunction(count, Float64)
) ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(ts_bucket)
ORDER BY (metric, host, ts_bucket)
TTL ts_bucket + INTERVAL 180 DAY;

CREATE MATERIALIZED VIEW mv_metrics_1h
TO metrics_1h
AS SELECT
    toStartOfHour(ts_bucket) AS ts_bucket,
    host,
    metric,
    sumMergeState(value_sum)   AS value_sum,
    maxMergeState(value_max)   AS value_max,
    countMergeState(value_count) AS value_count
FROM metrics_10min
GROUP BY ts_bucket, host, metric;

注意这里用的是 sumMergeState 而不是 sumState------因为输入已经是聚合状态,需要先 merge 再重新编码为 state。

查询路由策略

应用层根据查询的时间范围选择表:

复制代码
时间范围 <= 1 小时   → metrics_raw
时间范围 <= 24 小时  → metrics_10min
时间范围 <= 30 天    → metrics_1h
时间范围 > 30 天     → metrics_1d

也可以用 ClickHouse 的 VIEW 封装路由逻辑,但实践中应用层路由更灵活、更可控。


6. 为什么不能一张表打天下

"直接在原始表上 GROUP BY 不行吗?" 我们用数据说话:

假设场景:1000 台主机 × 50 个指标 × 每秒 1 条 = 5000 万条/天

查询 扫描原始表 扫描 10min 聚合表 加速比
最近 1 小时趋势 1.8 亿行 30 万行 600x
最近 7 天趋势 35 亿行 504 万行 700x
最近 30 天趋势 150 亿行 2160 万行 700x

除了扫描量,还有以下问题:

  • 原始表 TTL 3 天后数据就没了,30 天趋势根本查不到
  • 大范围 GROUP BY 消耗大量内存,可能触发 OOM
  • 并发查询时集群负载飙升,影响写入稳定性

多级 Rollup 的本质是用空间换时间,用写入时的计算换查询时的速度。在时序、监控、日志分析等场景中,这是标准做法。


总结

要点 说明
MV 是同步的 INSERT 路径中执行,影响写入延迟
用 AggregatingMergeTree 支持两阶段聚合,merge 时继续合并
State/Merge 函数配对 写入用 xxxState,查询用 xxxMerge
链式 MV 用 MergeState 从聚合表再聚合时用 xxxMergeState
查询路由 根据时间范围命中不同粒度的表
控制 MV 数量 超过 5 个需要评估写入链路压力

下一篇我们讨论 ClickHouse 中最容易踩坑的操作------UPDATE 和 DELETE。

相关推荐
uzong3 小时前
《大型网站技术架构》-大型网站技术架构背后的系统性思维(精华解读)
后端·架构
fire-flyer3 小时前
ClickHouse系列(八):ClickHouse 的 UPDATE / DELETE 正确姿势
大数据·数据库·clickhouse
心语星愿113 小时前
单片机架构:CPU、存储器与外设的协同原理
单片机·嵌入式硬件·架构
码云之上3 小时前
从 SQL DDL 到 ER 图:前端如何优雅地实现数据库可视化
前端·数据库·数据可视化
AKA__Zas3 小时前
SQL查询技巧全 Strategy Guide
数据库·sql·学习方法
luoganttcc3 小时前
华为 的 npu 架构如何 进行 flash attention
数据库·华为
Chasing__Dreams3 小时前
Mysql--基础知识点--94.1--嵌套子查询转关联查询
数据库·mysql
qq_283720053 小时前
Python 操作 MySQL 数据库全解:增删改查、事务、连接池与性能优化
数据库·python·mysql
无忧智库3 小时前
深度解码:华为IPD流程管理体系L1-L5最佳实践与数字化转型架构全景(PPT)
华为·架构