ClickHouse 架构与设计原理

ClickHouse 是面向 OLAP 场景的开源列式数据库,以极致的查询性能著称。本文详解 ClickHouse 的列式存储、向量化执行、MergeTree 引擎、分布式架构以及与行式数据库的本质区别。

1. ClickHouse 是什么?

ClickHouse 是一个快速开源的 OLAP(联机分析处理)数据库管理系统,由俄罗斯的 Yandex 开发并于 2016 年开源。它以极快的查询速度著称,在合理硬件配置下,单节点 ClickHouse 可以达到每秒处理数十亿行的性能。

ClickHouse 的核心特性:

  • 列式存储:数据按列存储,分析查询只需读取涉及的列,大幅减少 I/O
  • 向量化执行(Vectorized Execution):使用 SIMD 指令批量处理数据,而非逐行处理
  • MPP 架构(Massively Parallel Processing):查询在多个 CPU 核心和服务器上并行执行
  • 高效的压缩:列式存储使得同类型数据集中存储,压缩率远高于行式数据库
  • 实时数据摄入:支持每秒处理数百万行的数据写入
  • SQL 接口:兼容标准 SQL,降低学习成本

2. OLTP vs OLAP:场景的本质差异

理解 ClickHouse 的设计需要从 OLTP(事务处理)和 OLAP(分析处理)的区别说起:

维度OLTP(MySQL/PostgreSQL)OLAP(ClickHouse)
典型查询SELECT * WHERE id = ?SELECT sum(), avg() GROUP BY
数据修改频繁 UPDATE/DELETE批量写入,极少更新
数据规模GB 到 TBTB 到 PB
查询模式点查、短事务全表扫描、聚合计算
优化目标事务一致性、低延迟高吞吐、快速聚合
存储模型行式存储列式存储

形象地说,如果是管理订单的电商数据库(OLTP),我们会经常查询"订单 12345 的详情"。如果是分析用户行为的日志系统(OLAP),我们会查询"昨天每个地区的平均页面加载时间"。前者需要的是快速定位一行数据,后者需要的是扫描数十亿行并做聚合计算。

3. 列式存储的优势

3.1 存储模型对比

行式存储:

Row 1: [id=1, name="Alice", age=30, city="Beijing"]
Row 2: [id=2, name="Bob",   age=25, city="Shanghai"]
Row 3: [id=3, name="Carol", age=35, city="Beijing"]

磁盘上的物理布局(紧凑排列):

[1,"Alice",30,"Beijing"][2,"Bob",25,"Shanghai"][3,"Carol",35,"Beijing"]

列式存储:

Column "id":    [1, 2, 3]
Column "name":  ["Alice", "Bob", "Carol"]
Column "age":   [30, 25, 35]
Column "city":  ["Beijing", "Shanghai", "Beijing"]

磁盘上的物理布局(每列独立存储):

[id块]: 1,2,3          [name块]: "Alice","Bob","Carol"
[age块]: 30,25,35      [city块]: "Beijing","Shanghai","Beijing"

3.2 列式存储的查询优势

当执行 SELECT AVG(age) FROM users WHERE city = 'Beijing' 时:

  • 行式数据库:必须加载所有行的所有列(id、name、age、city),筛选 city 为 Beijing 的行,再计算 age 的平均值
  • 列式数据库:只需加载 age 列和 city 列,city 列扫描找出北京用户的行号,age 列直接对这些行号求平均值

在 100 列、10 亿行的表中,行式数据库需要读取 10 亿 × 100 列的数据,ClickHouse 只需读取 2 列的数据。

3.3 压缩效率

列式存储的第二个巨大优势是压缩率。因为同一列的数据类型相同,值域通常也相近,压缩算法可以发挥到极致:

  • 整数列:相邻行的值差异通常很小,Delta 编码后压缩率极高
  • 枚举列(如 city、status):字典编码后只需存储整数索引
  • 时间戳列:有序的时间序列数据经过 Delta-of-Delta 编码后体积大幅减少

ClickHouse 的压缩率通常为 1:5 到 1:10,意味着 10TB 的原始数据在 ClickHouse 中仅需 1-2TB 存储。这直接转化为:更少的磁盘 I/O → 更快的查询速度 → 更低的存储成本。

4. 向量化执行引擎

4.1 传统逐行执行 vs 向量化执行

传统数据库的执行方式被称为"火山模型(Volcano Model)":

for each row in table:
    check WHERE condition
    if true:
        add to result set
result = aggregate(result set)

这种方式的问题是 CPU 大部分时间花在了循环控制和分支预测上,而不是实际计算。

ClickHouse 的向量化执行每次处理一批(通常是 65536 行)数据:

data = read_column_batch(65536)
filtered = SIMD_compare(data.city, "Beijing")  # SIMD 并行比较
ages = SIMD_gather(data.age, filtered)          # SIMD 并行取数
result = SIMD_sum(ages) / SIMD_count(filtered)  # SIMD 并行计算

SIMD(Single Instruction Multiple Data) 是 CPU 的向量指令集(如 SSE、AVX),可以在一条指令中同时对多个数据执行相同操作。

4.2 为什么向量化执行快很多?

因素逐行执行向量化执行
CPU 分支预测频繁分支,预测失败率高批量处理,分支极少
缓存利用每行随机访问,缓存失效连续内存访问,缓存命中率高
SIMD 利用无充分使用 AVX-512
函数调用每行一次每批一次
内存带宽浪费在无关数据上只读需要的列

在理想情况下,向量化执行配合 AVX-512 可以同时处理 512/64 = 8 个 64 位整数的数据,理论加速比可达 8 倍。实际中由于内存带宽、分支处理和数据对齐等因素,通常可以获得 3-5 倍 的加速。

5. MergeTree 引擎家族

MergeTree 是 ClickHouse 最核心的表引擎。它的设计灵感来自 Google 的 LevelDB 和 Bigtable 的 LSM-Tree。

5.1 基本写入流程

写入数据 → 内存中的数据块(part)→ 按主键排序 → 异步合并(merge)
-- 创建 MergeTree 表
CREATE TABLE events (
    event_date Date,
    user_id UInt64,
    event_type String,
    value Float64
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(event_date)
ORDER BY (user_id, event_type);

当数据写入时:

  1. 数据首先按 ORDER BY 定义的键排序
  2. 数据被写入一个按 PARTITION BY 规则命名的新 part
  3. 后台进程周期性地将小的 part 合并成更大的 part(这个过程叫 merge)
  4. 合并后的 part 保持有序,且合并是后台异步进行的,不影响前台查询

5.2 Part 合并策略

初始写入:  [Part 1: 1000 rows] [Part 2: 1000 rows] [Part 3: 1000 rows]
第一次合并:[Part 4: 3000 rows]                               
新写入:    [Part 4: 3000 rows] [Part 5: 1000 rows] [Part 6: 1000 rows]
第二次合并:[Part 7: 5000 rows]                               

合并策略遵循分层规则:相同层级的 part 达到一定数量后合并成下一层级。这确保了 part 数量不会无限增长,同时也保证了新写入的数据能够快速查询(不需要等待大合并完成)。

5.3 主键与稀疏索引

ClickHouse 的主键和传统数据库的索引完全不同:

  • ClickHouse 主键:不保证唯一性,主要用于数据排序和范围剪枝
  • 稀疏索引(Sparse Index):每 8192 行存储一个索引标记(mark),而非每行一个索引项
数据(按 user_id 排序): [1,1,1,1,2,2,2,3,3,4,4,4,...]
                         ↑        ↑        ↑
                         mark 1   mark 2   mark 3
                         user_id∈[1,2] user_id∈[2,3] user_id∈[3,4]
                         
查询 WHERE user_id = 3:
  1. 检查 marks:mark2 的范围是 [2,3],mark3 的范围是 [3,4]
  2. 只需读取 mark2 和 mark3 对应的数据块
  3. 其他 marks 直接跳过

稀疏索引极大地减少了索引体积(只有每 8192 行一个条目,而非每行一个),同时仍能有效剪枝不需要的数据块。

6. 分布式架构

6.1 分片与副本

ClickHouse 的分布式架构涉及两个核心概念:

  • 分片(Shard):数据水平分割到不同服务器,每个分片包含数据的一个子集
  • 副本(Replica):每个分片的冗余拷贝,用于高可用和负载均衡
集群架构示例(2 分片 × 2 副本):

Shard 1:           Shard 2:
┌─────────────┐    ┌─────────────┐
│ Replica 1A  │    │ Replica 2A  │
│ (192.168.1.1)│    │ (192.168.1.3)│
└─────────────┘    └─────────────┘
┌─────────────┐    ┌─────────────┐
│ Replica 1B  │    │ Replica 2B  │
│ (192.168.1.2)│    │ (192.168.1.4)│
└─────────────┘    └─────────────┘

数据分布:ID % 2 == 0 → Shard 1    ID % 2 == 1 → Shard 2

6.2 分布式表

分布式表本身不存储数据,而是作为代理将查询路由到实际的分片:

-- 在每个分片上创建本地表
CREATE TABLE events_local ON CLUSTER my_cluster (
    event_date Date,
    user_id UInt64,
    event_type String
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}')
PARTITION BY toYYYYMM(event_date)
ORDER BY (user_id, event_type);

-- 创建分布式表(代理层)
CREATE TABLE events_distributed ON CLUSTER my_cluster AS events_local
ENGINE = Distributed(my_cluster, default, events_local, rand());

-- 用户查询分布式表
SELECT event_type, count() FROM events_distributed 
WHERE event_date > today() - 7 
GROUP BY event_type;
-- ClickHouse 自动将查询分发到所有分片,聚合结果后返回

7. ClickHouse 的局限性

了解 ClickHouse 的适用边界同样重要:

  • 不支持事务:没有 BEGIN/COMMIT/ROLLBACK,不适合需要 ACID 的场景
  • 不支持点更新/删除:虽然支持,但效率远低于行式数据库(异步后台操作)
  • JOIN 性能有限:跨分片 JOIN 性能较差,大表 JOIN 需要谨慎设计
  • 没有二级索引:只有主键的稀疏索引,点查需要扫描较多数据
  • 内存要求高:复杂查询需要大量内存用于聚合和排序

适用场景:日志分析、时序数据、数据仓库、监控指标、BI 报表
不适用场景:事务型业务、高频更新的用户资料、需要强一致性的订单系统

8. 总结

ClickHouse 通过以下设计在 OLAP 领域实现了极致性能:

设计决策收益
列式存储只读需要的列,减少 I/O
高效压缩降低存储成本,提高内存利用率
向量化执行充分利用 CPU SIMD,提高计算效率
稀疏索引小索引体积实现大范围剪枝
数据分区快速删除过期数据,分区级并行
异步合并写操作不阻塞,后台优化存储
MPP 架构查询水平扩展到多节点

对于需要从海量数据中提取洞察的业务——无论是网站用户行为分析、物联网设备监控、金融交易审计还是广告效果追踪——ClickHouse 都是值得深入研究的利器。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「数据库」更多文章

  1. ClickHouse 表引擎详解
  2. ClickHouse 监控与运维
  3. ClickHouse 生产案例与最佳实践