2. ClickHouse基础
在时序场景中,ClickHouse 之所以能在一台普通服务器上轻松吞吐每秒百万级写入、并在数十亿行数据上完成秒级聚合,根源并不在于某个“黑科技”,而在于它把列式存储、稀疏索引、分区裁剪与后台合并这几件事做到了极致。理解这一章,是后续所有查询优化与存储优化的地基。
一、列式存储原理:为什么时序数据天然适合按列组织
传统行式数据库把一行记录的所有字段连续存放,读取时即使只需要一个字段,也要把整行数据从磁盘拉进内存。而 ClickHouse 将每一列单独存储为独立的列文件,同一列的数据在物理上连续排列。对于时序场景,这意味着:查询 avg(cpu_usage) 时,磁盘只读取 cpu_usage 这一列,其他几十个字段完全不参与 I/O。
列式存储带来的第二个红利是压缩效率。同一列的数据类型一致、取值分布集中,非常适合用 LZ4、ZSTD 或 Delta、DoubleDelta、Gorilla 等编码压缩。时序数据中时间戳单调递增、指标值变化平缓,压缩比常常达到 10:1 甚至更高。第三个红利是向量化执行:ClickHouse 按列批量处理数据块(Block),配合 SIMD 指令,让 CPU 缓存命中率和指令流水线效率大幅提升。
二、MergeTree 引擎家族:ClickHouse 存储的核心骨架
MergeTree 是 ClickHouse 最核心的表引擎家族,几乎所有生产表都基于它或其变体。它的工作模型可以概括为:写入生成不可变的小 part,后台按规则持续合并成大 part。每个 part 内部按主键排序,并维护稀疏主键索引、分区目录和列文件。
MergeTree 家族的关键成员及适用场景如下:
| 引擎 | 核心能力 | 典型时序场景 |
|---|---|---|
| MergeTree | 基础排序、分区、稀疏索引 | 通用时序明细表 |
| ReplacingMergeTree | 按排序键去重,保留最新版本 | 设备状态快照、维表更新 |
| SummingMergeTree | 合并时对数值列求和 | 预聚合指标、计数器累加 |
| AggregatingMergeTree | 合并时按聚合函数状态合并 | 物化视图中的 uniq、avg 状态 |
| CollapsingMergeTree | 按 Sign 标记折叠抵消行 | 需要更新/删除语义的明细 |
| VersionedCollapsingMergeTree | 带版本号的折叠,支持乱序 | 乱序到达的变更数据 |
建表时最关键的三个参数是 ORDER BY、PARTITION BY 和 PRIMARY KEY。排序键决定数据物理顺序与稀疏索引粒度,分区键决定数据裁剪范围。时序表通常按时间分区(如 toYYYYMM(ts) 或 toDate(ts)),排序键以 (host, metric, ts) 这类“高基数维度 + 时间”组合最常见。
CREATE TABLE metrics.sample
(
ts DateTime,
host LowCardinality(String),
metric LowCardinality(String),
value Float64
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(ts)
ORDER BY (host, metric, ts)
TTL ts + INTERVAL 90 DAY;
三、数据类型与表引擎选择:从场景反推设计
ClickHouse 的数据类型直接影响存储体积与查询速度。时序场景中应优先使用最小可用类型:时间戳用 DateTime 或 DateTime64,枚举维度用 LowCardinality(String),整数按取值范围选 UInt8/UInt16/UInt32,浮点优先 Float32 而非 Float64。避免用 String 存时间或数值,否则既浪费空间又拖慢比较。
四、常见误区与复盘
- 误区一:分区键越细越好。按天分区在数据量不大时会产生大量小 part,反而增加合并压力。通常按月或按周分区更稳。
- 误区二:ORDER BY 随意写。排序键顺序决定索引裁剪能力,把低基数维度放前面会导致索引选择性差。
- 误区三:用 ReplacingMergeTree 当实时更新用。去重只在后台合并时发生,查询时可能仍看到重复行,需配合
FINAL或argMax使用。 - 误区四:忽略 TTL。时序数据无限增长会拖垮磁盘与查询,建表时就应规划 TTL 或冷热分层。