3. 时序数据建模
时序数据建模的核心目标只有一个:让写入足够快、让查询足够准、让存储足够省。ClickHouse 的 MergeTree 家族为此提供了极强的表达能力,但能力越强,越需要建模纪律。本节围绕四个关键决策展开:时间字段如何选、标签与指标如何分离、维度基数如何控制、分区键与排序键如何设计。
一、核心原理:时序模型的三层结构
一条时序记录本质上由三部分组成:时间戳(事件发生时刻)、标签(描述“谁/在哪/什么类型”的低频维度)、指标(随时间变化的数值)。ClickHouse 并不强制区分标签与指标,但物理存储会放大你的选择:标签决定排序与索引效率,指标决定压缩与聚合效率。
时间字段设计要区分三种语义:事件时间(event time)、采集时间(ingest time)、处理时间(process time)。绝大多数分析场景应以事件时间作为分区与排序的第一列,因为它决定了数据的时间局部性,也决定了分区裁剪能否生效。若用采集时间,迟到数据会打乱分区边界,导致查询扫描过多分区。
二、方法流程:从业务查询反推建模
建模不是先建表再想查询,而是先列出 Top 查询,再决定键的顺序。推荐流程如下:
- 列出查询模式:按时间范围过滤?按哪些标签过滤?是否需要按标签分组聚合?
- 确定时间粒度:秒级、毫秒级还是微秒级?ClickHouse 的 DateTime64 支持精度参数,但精度越高压缩率越低。
- 分离标签与指标:标签用 String 或 LowCardinality(String),指标用数值类型;避免把指标塞进标签列。
- 评估维度基数:对每个候选标签统计 distinct 值数量,超过百万级的谨慎放入排序键。
- 设计分区键:通常按月或按日分区,分区数量控制在几百到几千,避免过多小分区。
- 设计排序键:按“时间 + 高频过滤标签 + 中频标签”排列,让最常用的过滤条件走索引。
三、案例场景:设备监控表的建模对比
假设业务需要存储设备监控数据,字段包括:设备 ID、区域、设备类型、采集时间、温度、湿度、电压。查询主要是“某区域某类型设备在最近 7 天的温度趋势”。
不推荐的建表方式:把 device_id 放在排序键前面,因为设备 ID 基数极高,会导致索引条目爆炸,且按区域过滤时无法利用前缀。
-- 不推荐:高基数 device_id 前置
CREATE TABLE device_metrics_bad (
event_time DateTime,
device_id String,
region String,
device_type String,
temperature Float64,
humidity Float64,
voltage Float64
) ENGINE = MergeTree
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (device_id, event_time);
推荐的建表方式:时间在前,低基数标签居中,高基数设备 ID 放在排序键末尾或仅作为普通列。
-- 推荐:时间 + 低基数标签 + 指标
CREATE TABLE device_metrics (
event_time DateTime,
region LowCardinality(String),
device_type LowCardinality(String),
device_id String,
temperature Float64,
humidity Float64,
voltage Float64
) ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (region, device_type, event_time, device_id)
TTL event_time + INTERVAL 90 DAY;
| 设计项 | 不推荐做法 | 推荐做法 | 影响 |
|---|---|---|---|
| 时间字段 | 使用采集时间分区 | 使用事件时间分区 | 分区裁剪准确,迟到数据可控 |
| 标签类型 | 全部 String | 低基数用 LowCardinality | 字典编码,扫描更快 |
| 排序键 | 高基数 ID 前置 | 时间+低基数标签前置 | 索引小,前缀匹配高效 |
| 分区粒度 | 按小时分区 | 按月或按日分区 | 避免分区过多导致元数据压力 |
| 指标存储 | 指标混入标签列 | 指标独立数值列 | 压缩率高,聚合快 |
四、误区与复盘
常见误区二:把排序键当成“索引越多越好”。排序键只有前缀有效,后面的列几乎不参与裁剪。把十几个标签全部塞进排序键,只会增加写入合并成本,不会提升查询性能。
常见误区三:忽略维度基数。一个 region 可能只有几十个值,而 device_id 可能有上千万个。前者适合 LowCardinality 和排序键,后者适合作为普通列或放入跳数索引(skip index)辅助过滤。
复盘时可以用以下清单快速检查模型是否合理:
- 时间字段是否为事件时间,且作为分区与排序的第一决策依据?
- 标签是否与指标分离,指标是否使用数值类型?
- 排序键前缀是否覆盖最高频的过滤条件?
- 分区数量是否在可控范围(通常几百到几千)?
- 高基数维度是否被错误地放入了排序键前部?
- 是否配置了 TTL 或冷热分层策略来管理存储成本?
建模的最终检验标准是:用真实查询跑一遍 EXPLAIN 和查询日志,观察扫描行数、读取字节数和分区裁剪情况。如果扫描行数远大于结果行数,说明排序键或分区键没有起到应有的裁剪作用,需要回到查询模式重新调整键的顺序。