分析建模的取舍
分析库建模要先定两件事:事实表的粒度(一行代表什么业务事件)和常用过滤维度,然后决定预聚合到什么程度。ClickHouse 这类列式库不擅长高频 join,实践中常把维度冗余进事实表形成宽表,再用物化视图在写入时增量维护聚合结果;本质是用存储与写入复杂度换查询延迟。维度会变的业务要额外考虑回填路径。
基本信息
| 表角色 | 事实表(明细)、维表(属性)、宽表(冗余维度的明细)、汇总表(预聚合结果) |
|---|---|
| 排序键 | MergeTree 的 ORDER BY 决定稀疏索引与数据物理顺序,应把高频过滤维度放前面 |
| 预聚合手段 | 物化视图 + SummingMergeTree / AggregatingMergeTree,写入时增量更新 |
| 更新代价 | 列式库擅长大批量写入,单行更新昂贵;维表变更常靠整段回填 |
| 适用引擎 | ClickHouse MergeTree 家族、Doris、StarRocks 等列式分析引擎 |
典型使用场景
- 固定口径的看板与报表:把分钟级、小时级聚合固化,查询只读汇总表
- 明细下钻:保留明细表并设计排序键,让下钻查询能命中索引而非全表扫描
- 多租户分析:租户字段放在排序键首位,天然形成数据聚集并便于按租户搬迁
- 维度属性变更:用回填任务重算受影响时间段的宽表,而不是原地更新
- 指标口径治理:把去重方式、时区、迟到数据处理写进汇总表定义并随表版本记录
物化视图预聚合(ClickHouse 示例)
物化视图预聚合(ClickHouse 示例)
-- 明细表:按 (tenant, ts) 排序,既支撑明细查询也服务聚合回填
CREATE TABLE logs.events (
tenant String, ts DateTime, service LowCardinality(String),
ok UInt8, latency_ms UInt32
) ENGINE = MergeTree ORDER BY (tenant, ts);
-- 物化视图:写入时就地增量维护分钟级聚合,查询直接读汇总表
CREATE MATERIALIZED VIEW logs.events_1m
ENGINE = SummingMergeTree ORDER BY (tenant, service, minute)
AS SELECT tenant, service, toStartOfMinute(ts) AS minute,
count() AS cnt, sum(ok) AS ok_cnt, avg(latency_ms) AS latency_avg
FROM logs.events GROUP BY tenant, service, minute;
-- 历史数据不会自动进视图,需要手动回填一次
INSERT INTO logs.events_1m
SELECT tenant, service, toStartOfMinute(ts), count(), sum(ok), avg(latency_ms)
FROM logs.events GROUP BY tenant, service, toStartOfMinute(ts);建模方式对照
| 方式 | 优点 | 代价 |
|---|---|---|
| 宽表 | 查询不做 join,链路短、延迟低 | 维度更新要回填;存储冗余放大 |
| 星型模型 | 维表独立,更新简单 | 查询 join 成本高,列式库上性能不稳 |
| 预聚合表 | 查询快,扫描量小 | 口径必须与明细一致;回填与版本管理不可省 |
| 明细表 + 排序键 | 支持任意维度下钻 | 没有现成聚合时可查范围大,依赖排序键设计 |
预聚合的口径要写清楚:去重方式、时区、是否含迟到数据、保留几位小数。口径含糊的汇总表在业务与明细对账时会互相打架,且很难判断是哪一侧算错。
物化视图只对创建之后的写入生效,历史数据必须单独回填一次,否则汇总表会安静地缺一段,查询结果看起来正常但偏低。