存储设计

列式存储架构

列式存储通过物理隔离各列数据来大幅减少无效的磁盘 I/O 操作。与行式存储将整行数据打包存放不同,列式存储将同一列的数据连续存放在独立的磁盘文件中。OLAP 分析通常只涉及宽表中的少数几列,当执行聚合查询时,系统只需加载查询涉及的列文件,有效避免了读取多余数据的开销。

同列数据类型的天然一致性为高效的数据压缩提供了良好条件。ClickHouse 默认采用 LZ4 压缩算法,在压缩率和解压速度之间取得了良好的平衡。连续存放的相似数据使得压缩算法能够轻易找到重复模式,从而显著降低存储占用,进一步缓解了磁盘 I/O 瓶颈。

数据片段与合并机制(MergeTree)

ClickHouse 采用先攒批再落盘的机制来提升数据写入效率。系统在接收到数据时不会逐条实时落盘,而是先在内存中积攒成批,随后作为一个完整的数据片段(Data Part)批量写入磁盘。后台进程会定期将这些零散的小片段合并成更大的片段。

  • 写入过程:客户端发起的批量写入请求会直接生成新的物理文件。这种设计避免了对现有数据文件的修改,属于纯粹的顺序追加写,从而保障了优异的写入吞吐量。
  • 合并策略:后台合并线程会异步监控数据片段的数量与大小。当小片段积累到一定阈值时,系统会将同一分区内的多个片段合并成一个大片段,并在合并过程中执行数据去重与过期清理等维护任务。

数据分区(Partitioning)

数据分区是 ClickHouse 在物理层面上隔离数据的重要机制。合理的分区设计能够在查询时快速跳过无关目录,实现高效的查询裁剪。同时,系统支持以分区为单位配置数据生命周期管理(TTL),方便业务低成本地清理或归档历史数据。

推荐使用低基数的时间维度字段作为分区键。理想的分区策略应该将单表的分区总数控制在合理范围内,通常建议保持在几十到数百个之间,从而避免底层文件系统过载。

为什么分区不能使用基数较多的字段?

避免因分区数量过多导致底层文件碎片化。ClickHouse 按分区在磁盘上组织目录,如果分区字段基数过大(例如使用用户 ID),会产生海量微小分区,这不仅会增加后台合并负担,还会降低整体查询效率。

索引设计

传统关系型数据库(OLTP)通常会为每一行数据建立主键索引,这种设计虽然利于精准查询,但往往会成为高并发写入的性能瓶颈。每次插入新数据时都需要实时更新和维护庞大的索引结构,带来了高昂的维护成本。

ClickHouse 依托 MergeTree 引擎转变了思路,放弃了逐行建索引的做法以平衡海量数据的读写需求。它采用了稀疏索引,不再记录每一条数据的具体位置,而是按区块记录数据的分布范围,在保证数据高效写入的同时维持了良好的查询性能。

主键索引

ClickHouse 的主键索引本质上是稀疏索引。传统数据库采用稠密索引为每一行记录建立索引,虽然定位单行数据快,但索引本身容易变得非常庞大。而 ClickHouse 则是把数据按批次分成多个颗粒(Granule),只记录每个颗粒中主键的最大值和最小值。当执行查询时,它能迅速跳过那些不包含目标数据的数据块,从而有效减少磁盘读取量。

主键索引的顺序决定了数据过滤的效率,它基本遵循从左到右的前缀匹配原则。

比如:

1
2
PRIMARY KEY (dt, app_id, user_id)
ORDER BY (dt, app_id, user_id)

查询条件只要从主键最左侧开始命中,就能最大化利用索引加速:

1
2
3
4
5
6
7
8
WHERE dt = '2026-07-01'

WHERE dt = '2026-07-01'
AND app_id = 123

WHERE dt = '2026-07-01'
AND app_id = 123
AND user_id = 456

精简的主键能够降低内存占用,因为主键越长对应的稀疏索引越大。后面的字段如果很少用于数据过滤,就不值得塞进主键索引里。

排序索引可以比主键索引包含更多字段。这样主键索引用前两个字段控制扫描范围,完整的排序索引继续改善数据局部性和压缩率。

1
2
PRIMARY KEY (dt, app_id)
ORDER BY (dt, app_id, user_id, event_time)

排序索引是建表时指定的 ORDER BY 字段。ClickHouse 写入数据时会在每个数据片段(Data Part)内按照指定的规则排序。后台合并多个数据片段时,也会继续维持这个全局排序。

排序索引主要有以下作用:

  • 提升数据压缩率:相邻且相似的数据存放在一起,有助于底层压缩算法发挥更好效果
  • 优化范围查询:有序数据使得范围扫描能够集中在连续的磁盘块上,减少随机读取

主键索引和排序索引的联系与区别:

  • 如果只指定了排序键,主键会被隐式定义为与排序键相同
  • 如果同时指定了主键和排序键,主键必须是排序键的前缀
  • 主键索引主要用于减少查询时的磁盘读取量,排序索引主要用于优化存储的局部性和排序操作

跳数索引

跳数索引(Data Skipping Index)是主键索引的灵活补充,主要用于应对主键难以覆盖的多维查询场景。主键在建表固化后通常无法更改,当业务衍生出全新的查询维度时,既有主键往往难以发挥加速作用。相比之下,跳数索引支持在建表后动态挂载,能够随着业务的演进随时调整,为各类非主键查询提供良好的性能支持。

跳数索引可用于多个连续粒度块的层级存储少量元数据,从而避免扫描无关行。

为满足不同数据类型的查询需求,ClickHouse 提供了六种跳数索引:

  • minmax:记录每个数据片段的最大与最小值,用于快速排除不在范围内的片段
  • set(N):保存每个数据片段中最多 N 个去重后的值,用于低基数字段的精确匹配
  • text:建立针对字符串分词的倒排索引,用于加速文本数据的全文检索
  • bloom_filter:通过概率模型快速判断数据是否可能存在,用于大基数字段的存在性校验
  • ngrambf_v1:提取字符串的连续片段来构建布隆过滤器,用于加速模糊查询
  • tokenbf_v1:提取字符串的独立词汇来构建布隆过滤器,用于更轻量级的全文匹配

向量化执行

向量化执行(Vectorized Execution)是 ClickHouse 实现极速查询的核心技术之一。与传统数据库逐行处理数据的火山模型不同,向量化引擎以数据块(Block)为单位进行批量处理。这种按列组织的数据块非常适合现代 CPU 的 SIMD(单指令多数据流)指令集,能够在单条指令中同时对多条数据进行相同的运算,从而成倍提升 CPU 的执行效率。

主要优势包括:

  • 减少函数调用开销:处理一个数据块只需一次函数调用,而非每行调用一次
  • 提升缓存命中率:连续的列式数据更利于 CPU 缓存预取
  • 充分利用 SIMD 指令:编译器能够更好地将循环计算优化为向量化指令