$Wynn5a 技术博客 - AI编程与软件工程实践
~/blog/clickhouse-paper-lightning

为什么 ClickHouse 那么快(1)

引言#

ClickHouse 官方的论文 ClickHouse - Lightning Fast Analytics for Everyone 解释了为什么 ClickHouse 能够在大规模数据量(PB 级别、上百列的数据表)中实现闪电般的查询和分析速度

摘要#

  • 问题背景:数据规模呈指数级增长,企业需要在成本可控、可扩展的前提下同时管理历史与新增数据,并在高并发下实现接近实时(用例相关的亚秒级)查询时延
  • 系统定位:ClickHouse 是面向 PB 级与高写入速率场景的开源 OLAP 数据库
  • 存储层:采用基于传统 LSM 思想的数据格式,并在后台对历史数据持续做聚合、归档等转换,以降低成本、优化读写
  • 查询层:提供易用的 SQL 方言,配合先进的向量化执行引擎,并可选择进行代码编译以提升性能
  • 数据裁剪:积极使用多种裁剪技术,尽量避免在查询时处理无关数据
  • 生态集成:可在表函数、表引擎或数据库引擎层面与其他数据管理系统集成
  • 效果:真实世界的基准测试显示,ClickHouse 处于当前分析型数据库中的领先性能行列

系统介绍#

ClickHouse 是一款面向高性能分析的列式 OLAP 数据库,目标是在“万亿行、百列”的表上提供亚秒级查询;项目起始于 2009 年(最初为网络日志过滤与聚合组件),2016 年开源。

ClickHouse 的设计聚焦于现代分析型数据管理场景的五类关键挑战:

海量数据与高速率写入#

这个挑战要求数据库既要保持数据压缩和索引的高效,又可横向扩展(scale-out),同时因为数据冷热对分析结果也很重要,数据库要能在保持对新数据高速率摄入的同时,对历史数据进行后台聚合/归档类的“降级”处理,与之同时,不能拖慢并发查询

大量并发且低延迟查询#

查询通常可以分为临时查询或定期查询。越是交互性强的用例,查询的延迟要求越低,而重复性的查询提供了调整数据的物理布局以适应业务负载的机会。

这个挑战要求数据库应该提供剪枝技术来优化频繁出现的查询,同时根据查询的优先级,数据库还必须动态调整共享系统资源(如 CPU、内存、磁盘和网络 I/O)的访问优先级,即使同时运行大量查询也要如此

异构数据生态#

这个挑战要求数据库保持开放性,能够读写多种外部系统、位置与格式,便于融入现有数据架构

友好的查询语言与性能自省#

这个挑战要求数据库提供表达力强的 SQL 方言(含嵌套类型、窗口/聚合等)来与数据交互,同时,数据库还应该提供强大的工具链,以便用户深入分析系统或查询的性能

工业级可靠性与多样化部署#

由于普遍的硬件不可靠性,数据库必须通过数据复制来增强应对节点故障的鲁棒性,同时数据库应该在多种规格的硬件上运行,最好是使用对应平台的原生二进制执行文件来部署

架构#

分层与组件#

核心引擎分为三大层——查询处理层(Sec.4)、存储层(Sec.3)、集成层(Sec.5),此外,访问层负责会话与多协议通信。

旁路组件涵盖线程调度、缓存、RBAC、备份与监控,数据库整体以单一静态链接的 C++ 可执行文件交付。

查询处理#

按“解析→逻辑/物理计划→执行”的传统流水线;采用向量化执行模型并辅以机会式 LLVM 代码生成;支持增强型 SQL 方言、PRQL 与 Kusto 的 KQL 来查询

存储层#

存储层由不同的表引擎组成,表引擎封装了表数据的格式和位置。表引擎有以下三类:

  • MergeTree*:主要的持久化格式,借鉴 LSM,将表按有序不可变 part拆分并由后台持续合并,不同变体在合并策略上各异,如聚合或替换过期行
  • 特用表引擎:用于加速/分布式执行,比如字典(in-memory KV)、纯内存临时表,以及用于透明分片的 Distributed 引擎
  • 虚拟表引擎:主要用于与外部系统双向交换数据(PostgreSQL/MySQL、Kafka、Redis)、数据湖(Iceberg/Delta/Hudi)与对象存储(S3/GCP/Azure)

可扩展与高可用#

ClickHouse 支持将表在集群的不同节点之间进行分片和复制,以保证扩展性和可用性

  • 分片(Sharding):按表达式将表划分为多个独立 shard,可直接读写或通过 Distributed 获得全局视图,主要用于突破单机容量与读写负载均衡
  • 复制(Replication):通过 ReplicatedMergeTree* 与 Keeper(Raft 共识、ZooKeeper 的 C++ 替代)保障每个 shard 维持可配置副本数

部署与接入#

支持本地(单机/多节点)、云托管(ClickHouse Cloud)、独立命令行与进程内 chDB模式,客户端可用原生协议、MySQL/PostgreSQL 线协议或 HTTP REST

存储层#

本节将讨论 MergeTree 表引擎作为 ClickHouse 的原生存储格式的一些细节,接下来介绍持续转换数据但不影响同时插入的合并策略,最后,将解释更新和删除的实现方式,以及数据去重、数据复制和 ACID 兼容性

存储格式#

  • 不可变 Part 与后台合并:每次插入(一个批次)生成一个不可变 part;为控制 part 数量,后台定期将小 part 合并成更大的 part,直到达到可配置目标(默认约 150 GB)。每个 part 自包含解释其内容所需的元数据
  • 同步 / 异步插入:同步模式下每次 INSERT 直接落盘为新 part,官方建议批量写入(如 2 万行)以降低合并开销;为实时小批量上报(如观测/指标)场景,异步模式可跨多次 INSERT 先缓冲,超阈值或超时再生成 part
  • 与 LSM 的差异:ClickHouse 不采用分层/分级结构,所有 part 等价,合并不受“层级”限制;由于放弃按层时间顺序,更新/删除不依赖墓碑而需其他机制;此外,插入直接写盘而非常见的 WAL。
  • 列式物理布局:一个 part 对应一个目录,默认每列一文件;小于 10 MB 的小 part 可合并为单文件以提升局部性。行被逻辑分组为 granule(最小不可分处理单元),一个 granule 默认 8192 行。实际 I/O 以 block 为粒度:同列相邻若干 granule 组成 1 MB(默认,可配)的压缩块。默认压缩 LZ4,也可选 Gorilla/FPC 等特定编解码,并支持编解码链(如 delta→重压缩→AES 加密)。为在压缩下保持随机访问,每列存有从 granule指向其压缩块偏移与解压后偏移的映射。
  • 列类型包装与分区:支持 LowCardinality(T)(字典化以降存储)与 Nullable(T)(位图管理空值);表可按范围/哈希/轮询自由分区,并为每个分区保存分区表达式的min/max 以启用分区裁剪,可选 HLL、t-digest 等统计

数据裁剪#

ClickHouse 提供三类裁剪机制:主键索引、投影(Projection)、跳读索引(Skipping Index),用于在查询时跳过大部分无关行以显著加速

  • 主键索引(稀疏、局部有序)
    • 每个 part 内按主键列排序(局部聚簇);同时保存“每个 granule 首行的主键值 → granule id”的稀疏映射,体量小到常驻内存(示例:810万行≈1000条索引项)
    • 面向等值/范围谓词,用二分查找替代顺扫;排序还能在合并与物理计划中复用(如移除排序操作符)
    • 例:在 EventTime 上的范围过滤通过主键索引二分定位命中的 granule。
  • 投影(Projection)
    • 同一张表的备用排序版本(不同主键),优化在“非主表主键列”上的过滤查询,代价是增加写入、合并与存储开销
    • 默认仅懒加载新写入的 part,存量数据需手动物化;优化器基于估算 I/O 成本在主表与投影间择优,缺失投影时回退到主表
  • 跳读索引(Skipping Index)
    • 轻量级投影,理念是在若干连续 granule 的层级存少量元数据,按可配置粒度跳过无关区段
    • 跳读索引表达式可自定义,提供很高的自由度
    • 可用的跳读索引类型包括:① Min-max 记录区间最小/最大值,适合局部聚簇、取值范围小的列;② Set 记录块内有限个唯一值,适合局部低基数;③ Bloom(行/词元/n-gram)适合文本查找,但不支持范围或否定谓词。

合并期数据变换#

  • 动机与机制:为兼顾高写入与历史数据降级,ClickHouse 在后台合并(merge)时持续、增量地转换既有数据(聚合、归档/老化等),不影响 INSERT 吞吐;若需强一致的查询结果,可在 SELECT 中加 FINAL 将这些变换在读时补齐
  • Replacing merges(去重/保留最新):按主键判等,仅保留同键最新版本的行;“最新”默认以所在 part 的创建时间判定,也可显式指定版本列以获得可控的保留规则
  • Aggregating merges(预聚合):将主键相同的多行折叠为聚合态的一行;非主键列存放部分聚合状态(如 sumState、countState),合并时再按语义组合;常配合物化视图实现插入即增量更新,最后用 …Merge 族函数在读时求出最终值。
  • TTL merges(按生命周期老化):基于触发条件(行级时间表达式)与动作在合并时处理单个 part:可迁移到冷存储重压缩删除滚动聚合(roll-up);示例展示了数据一周后迁移至 S3 的规则

更新和删除#

MergeTree* 引擎们以追加写入为优先,但在像合规等场景需要少量修改。ClickHouse 提供两条路径,且都不阻塞并发写入

  • Mutations(变更):对表的所有 part 做就地重写;为避免数据量临时翻倍,此操作非原子,期间并发查询可能同时读到已变更与未变更的 part;完成后底层数据被物理改写删除型变更成本高,因为要重写所有 part 的所有列
  • Lightweight Delete(轻量删除):只更新内部位图列并在查询时自动追加过滤,被标记的行在未来某次合并中才被物理清理;通常比变更快,但会带来一定查询开销

    含投影的表默认不启用轻量删除,以免投影与主表不一致;可用 lightweight_mutation_projection_mode 控制(如允许自动丢弃投影),注意要评估投影价值与删除频率后再开启

  • 并发语义:同一表上的更新/删除应少见且串行,以避免逻辑冲突

幂等插入#

  • 真实场景里客户端在发送 INSERT 后若发生超时,难以判断是否写入成功;传统解法是重发并依赖主键/唯一约束做逐点查重,但这要求对全量元组建立索引,在大数据+高写入下空间与更新开销过高。
  • ClickHouse 的做法:把每次插入最终形成的 part 视为去重单元,仅记录最近 N 个已插入 part 的哈希(如 N=100),若重发批次的哈希已存在则忽略;非复制表在本地存放,复制表借助 Keeper 存放。客户端也可显式传入 insert token 作为 part 哈希,这样即可安全重试,实现幂等插入。计算哈希有少量 CPU 开销,但存储/比较成本可忽略
  • 对于“版本去重/只保留最新”这类语义级去重,应结合 3.3 的 Replacing merges 等机制,而不是依赖插入期去重。

数据复制#

  • 目的:复制用于高可用(容错)、负载均衡与零停机升级
  • 状态与日志:复制以“表状态=一组表 part + 元数据”为基本单元;三类本地操作推进状态(①插入→新增 part;②合并→新增结果 part 并删除输入 part;③变更/DDL→增删 part 或改元数据),并将这些状态转移顺序写入全局复制日志。其他节点异步回放该日志,因此最终一致。需要更强保证时,可把操作设为同步直至达到 quorum(如多数或全体副本)
  • 协调层:复制日志由通常三个 ClickHouse Keeper 进程维护,Keeper 基于 Raft 提供容错协调;各节点起始指向同一日志位置,随后本地执行并让其他副本异步追赶
  • 流程示例:节点1写入并记两条日志;节点2拉取首条并从节点1抓取新 part;节点3依次拉取两条、抓取两份数据;随后在节点3本地合并为新 part、删除旧 part,并把“合并”写回日志
  • 三项优化:① 新增节点直接拷贝最新写入者的表状态而非从头回放;② 合并可选择本地重做直接抓取合并结果以在 CPU 与网络间权衡(跨机房通常偏好本地合并以降带宽);③ 互不依赖的日志项可并行回放

ACID 兼容性#

  • 并发读写模型:为最大化并发性能,ClickHouse 尽量避免加锁;查询在开始时对所涉表的全部 part拍下快照,期间产生的新 part(并发 INSERT、合并)不参与本次执行;为避免并发修改/删除,执行期会提升相关 part 的引用计数。这一机制对应于基于版本化 part 的 MVCC下的快照隔离
  • 非完全 ACID:除少数“并发写各自只影响单一 part”的特殊情形外,一般语句不满足完整 ACID
  • 耐久性取舍(默认不 fsync):实际高写入场景通常能容忍断电导致的少量新数据丢失风险;ClickHouse 默认不强制 fsync 新插入的 part,交由内核批量落盘,以吞吐换原子提交
  • 与 WAL 的关系:论文指出 ClickHouse 的插入直接写磁盘,不同于许多基于 LSM 的系统依赖 WAL

查询处理层#

ClickHouse 在数据项、数据块和表分片级别并行化查询。多个数据项可以使用 SIMD 指令在算子内同时处理。在单个节点上,查询引擎在多个线程中同时执行操作符。ClickHouse 使用与 MonetDB/X100 [11]相同的向量化模型,即操作符生成、传递和消费多行(数据块)而不是单行,以最小化虚函数调用的开销。如果源表被分割成不相交的表分片,多个节点可以同时扫描这些分片。因此,所有硬件资源都得到充分利用,查询处理可以通过添加节点进行水平扩展,通过添加核心进行垂直扩展。

SIMD 并行化#

  • ClickHouse 在算子间传递“多行批”(data chunks),从而为 向量化 创造条件;向量化既可通过手写 SIMD intrinsics,也可依赖编译器自动向量化。热点循环通常被编译为多种内核实现(非向量化 / AVX2 / 手写 AVX-512),并在运行时基于 CPUID 自动选择最快版本(CPU dispatch)。论文同时强调:系统可在至少 SSE 4.2 的老旧 CPU 上运行,但在新硬件上能显著提速。
  • 论文脚注链接到“CPU Dispatch”实践:官方工程文章阐述了如何在 ClickHouse 中按指令集(SSE4.2、AVX、AVX2、AVX-512)做运行时选择与热点优化,这也是 4.1 描述的实现基础。