2. ClickHouse基础

在时序场景中,ClickHouse 之所以能在一台普通服务器上轻松吞吐每秒百万级写入、并在数十亿行数据上完成秒级聚合,根源并不在于某个“黑科技”,而在于它把列式存储、稀疏索引、分区裁剪与后台合并这几件事做到了极致。理解这一章,是后续所有查询优化与存储优化的地基。

一、列式存储原理:为什么时序数据天然适合按列组织

传统行式数据库把一行记录的所有字段连续存放,读取时即使只需要一个字段,也要把整行数据从磁盘拉进内存。而 ClickHouse 将每一列单独存储为独立的列文件,同一列的数据在物理上连续排列。对于时序场景,这意味着:查询 avg(cpu_usage) 时,磁盘只读取 cpu_usage 这一列,其他几十个字段完全不参与 I/O。

列式存储带来的第二个红利是压缩效率。同一列的数据类型一致、取值分布集中,非常适合用 LZ4、ZSTD 或 Delta、DoubleDelta、Gorilla 等编码压缩。时序数据中时间戳单调递增、指标值变化平缓,压缩比常常达到 10:1 甚至更高。第三个红利是向量化执行:ClickHouse 按列批量处理数据块(Block),配合 SIMD 指令,让 CPU 缓存命中率和指令流水线效率大幅提升。

列式存储的本质是“按需读取 + 同质压缩 + 批量计算”。它牺牲的是单行点查和频繁更新的性能,换来的却是海量数据扫描聚合的极致速度——这正好契合时序分析“写多读少、读多为聚合”的特征。
行式存储 vs 列式存储:物理布局对比 行式存储 ts1 | host1 | cpu | 12.5 | mem | 60 ts2 | host1 | cpu | 13.1 | mem | 61 ts3 | host2 | cpu | 11.8 | mem | 58 读取 cpu 列需扫描全部行数据 列式存储 ts host cpu mem 读取 cpu 列只扫描该列文件 列式优势:① 按需 I/O,只读相关列;② 同质数据高压缩比(LZ4/ZSTD/Delta/Gorilla); ③ 向量化批量计算,SIMD 加速;④ 天然适配聚合与范围扫描。

二、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 存时间或数值,否则既浪费空间又拖慢比较。

选择表引擎的判断路径:先问“是否需要去重/聚合/折叠语义”,再问“数据是否乱序到达”,最后问“是否需要 TTL 与分区裁剪”。若只是纯明细写入与范围聚合,基础 MergeTree 就是最优解,不要为了“看起来高级”而叠加复杂引擎。

四、常见误区与复盘

  • 误区一:分区键越细越好。按天分区在数据量不大时会产生大量小 part,反而增加合并压力。通常按月或按周分区更稳。
  • 误区二:ORDER BY 随意写。排序键顺序决定索引裁剪能力,把低基数维度放前面会导致索引选择性差。
  • 误区三:用 ReplacingMergeTree 当实时更新用。去重只在后台合并时发生,查询时可能仍看到重复行,需配合 FINAL 或 argMax 使用。
  • 误区四:忽略 TTL。时序数据无限增长会拖垮磁盘与查询,建表时就应规划 TTL 或冷热分层。
复盘要点:MergeTree 的 part 是不可变的,更新和删除都是“写入新版本 + 后台合并”的异步过程。任何依赖“立即生效”的业务逻辑,都必须显式处理合并延迟,否则会出现数据看似重复或未更新的现象。