存储设计
列式存储架构
列式存储通过物理隔离各列数据来大幅减少无效的磁盘 I/O 操作。与行式存储将整行数据打包存放不同,列式存储将同一列的数据连续存放在独立的磁盘文件中。OLAP 分析通常只涉及宽表中的少数几列,当执行聚合查询时,系统只需加载查询涉及的列文件,有效避免了读取多余数据的开销。
同列数据类型的天然一致性为高效的数据压缩提供了良好条件。ClickHouse 默认采用 LZ4 压缩算法,在压缩率和解压速度之间取得了良好的平衡。连续存放的相似数据使得压缩算法能够轻易找到重复模式,从而显著降低存储占用,进一步缓解了磁盘 I/O 瓶颈。
数据片段与合并机制(MergeTree)
ClickHouse 采用先攒批再落盘的机制来提升数据写入效率。系统在接收到数据时不会逐条实时落盘,而是先在内存中积攒成批,随后作为一个完整的数据片段(Data Part)批量写入磁盘。后台进程会定期将这些零散的小片段合并成更大的片段。
- 写入过程:客户端发起的批量写入请求会直接生成新的物理文件。这种设计避免了对现有数据文件的修改,属于纯粹的顺序追加写,从而保障了优异的写入吞吐量。
- 合并策略:后台合并线程会异步监控数据片段的数量与大小。当小片段积累到一定阈值时,系统会将同一分区内的多个片段合并成一个大片段,并在合并过程中执行数据去重与过期清理等维护任务。
数据分区(Partitioning)
数据分区是 ClickHouse 在物理层面上隔离数据的重要机制。合理的分区设计能够在查询时快速跳过无关目录,实现高效的查询裁剪。同时,系统支持以分区为单位配置数据生命周期管理(TTL),方便业务低成本地清理或归档历史数据。
推荐使用低基数的时间维度字段作为分区键。理想的分区策略应该将单表的分区总数控制在合理范围内,通常建议保持在几十到数百个之间,从而避免底层文件系统过载。
为什么分区不能使用基数较多的字段?
避免因分区数量过多导致底层文件碎片化。ClickHouse 按分区在磁盘上组织目录,如果分区字段基数过大(例如使用用户 ID),会产生海量微小分区,这不仅会增加后台合并负担,还会降低整体查询效率。
索引设计
传统关系型数据库(OLTP)通常会为每一行数据建立主键索引,这种设计虽然利于精准查询,但往往会成为高并发写入的性能瓶颈。每次插入新数据时都需要实时更新和维护庞大的索引结构,带来了高昂的维护成本。
ClickHouse 依托 MergeTree 引擎转变了思路,放弃了逐行建索引的做法以平衡海量数据的读写需求。它采用了稀疏索引,不再记录每一条数据的具体位置,而是按区块记录数据的分布范围,在保证数据高效写入的同时维持了良好的查询性能。
主键索引
ClickHouse 的主键索引本质上是稀疏索引。传统数据库采用稠密索引为每一行记录建立索引,虽然定位单行数据快,但索引本身容易变得非常庞大。而 ClickHouse 则是把数据按批次分成多个颗粒(Granule),只记录每个颗粒中主键的最大值和最小值。当执行查询时,它能迅速跳过那些不包含目标数据的数据块,从而有效减少磁盘读取量。
主键索引的顺序决定了数据过滤的效率,它基本遵循从左到右的前缀匹配原则。
比如:
查询条件只要从主键最左侧开始命中,就能最大化利用索引加速:
精简的主键能够降低内存占用,因为主键越长对应的稀疏索引越大。后面的字段如果很少用于数据过滤,就不值得塞进主键索引里。
排序索引可以比主键索引包含更多字段。这样主键索引用前两个字段控制扫描范围,完整的排序索引继续改善数据局部性和压缩率。
排序索引是建表时指定的 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 指令:编译器能够更好地将循环计算优化为向量化指令