ClickHouse架构学习笔记:系统定位/存储模型/为什么快/常见问题

发布时间:2026/7/7 1:12:33
ClickHouse架构学习笔记:系统定位/存储模型/为什么快/常见问题 clickhouse以下简称CH是互联网企业常见的OLAP引擎。因此我抱着好奇的态度开始学习CH。这篇笔记的知识来源于CH的官网和AI对话如果大家想系统性地学习CH那从CH官方文档开始是最合适的免费且系统我尝试过找CH的书籍没有找到很权威的甚至还有中文版还有详尽的示意图。如果对某个特定的知识点有疑问那可以用AI进行深入的追问。当我希望开始接触CH时脑海里大概是这些问题希望大家也可以有自己的问题带着问题去学习CHCH在数据系统中的地位是什么计算有spark/flink存储有parquet/表格式等那CH服务于哪一环经典面试题CH为什么快尤其想了解的是存储结构上有何特别之处大数据领域的快通常和存储模型有极大的关系如果不能快速跳过无关数据那计算再快也没用CH的分布式模型是什么样的CH有哪些竞品与竞品有何区别是相互取代还是互补关系这篇笔记结构分为四部分CH 架构/存储模型、计算模型、使用技巧、QA。CH的定位从传统数仓链路说起以下如果说到数仓可能指的是数仓团队/数仓链路当说到数仓由多个部分组成时也可能指的是数仓产品比如snowflake/CH需要按语境仔细甄别一个传统数仓系统通常有ETL也可能ELT以下只称ETL步骤。ETL的存在是因为传统数仓只能处理结构化数据支持的数据源类型也有限而外界数据不仅来源多样、格式多样甚至可能有非结构化数据。为了将五花八门的原始数据转换为数仓可接受的结构化数据格式需要有一套ETL清洗pipeline完成从数据源加载Extract、转换Transform或者叫清洗然后将清洗后的结果存到数仓中Load数仓载入数据。最后数仓为数据分析师等分析/算法/报表团队提供查询服务他们可以跑临时SQL来探索数据也可以从已有表出发做计算得到另一张表还可以搭建报表、看板等。大致链路如下图所示数据源 - 加载清洗 - 数仓 - 各种查询服务这边的第一个问题就是为啥非得用数仓来提供查询服务不能直接从源数据查吗省的这么多数据工程师的人力劳动了答案是不行。一方面很多用数仓的用户并不太懂技术或者只会使用SQL进行数据分析和处理所以数仓提供给他们一个统一的数据处理环境不需要太多额外的知识另一方面数仓只用考虑结构化数据通常查询性能比较高通常可以在秒级从数十亿行的表里算出某个指标这听起来像魔法实则利用了很多方式跳过无关数据。因此不管从性能还是易用性上数仓系统都是必须的。第二个问题是ETL和ELT的差别是什么随着数仓系统的多年发展数仓不是只能导入结构化数据了很多数仓系统拥有从各类数据源接数据的功能。因此转换或者说清洗步骤就可以后移到数仓里执行。这里的差别是字母L所在的位置界定了T是发生在进数仓前还是进数仓后。这更像是云厂商的广告文字游戏个人认为大部分时候不用特别区别ETL/ELT两者的区别数仓和OLAP数据库的差别我觉得二者的界限很模糊几乎可以划等号。数仓肯定是为了支持OLAP负载而生的。如果非要咬文嚼字一下那传统数仓系统算是OLAP系统中的一种架构实现OLAP系统还可以是数据湖架构等。不过当提到“我们是做数仓的”其实通常指的也是搭建数仓链路的具体实现可以是数仓也可以是数据湖。CH处于哪里CH可以是数仓也可以只是查询服务层。这是因为CH主要特点是极快的查询速度而对调度、复杂计算的支持有限所以它经常可以在一些日志查询系统里见到。比如如下架构日志源 - Flink(可选) - CH - 快速查询CH 架构存储模型ClickHouse 更像是面向 OLAP 的列式查询引擎而不是完整的数据仓库平台。它擅长大宽表、明细查询、聚合分析和维度表关联但不适合复杂大表 Join、多层依赖 Pipeline、Graph Transaction、CDC 主链路也没有内置 DAG 调度器。因此在数据平台里ClickHouse 通常更适合作为“存储 查询”层上游用 Airbyte、Debezium、Flink、Glue、Spark 或 dbt 完成接入和转换再把查询友好的结果写入 ClickHouse。这一章的核心问题Merge Tree引擎和LSM Tree的异同如果了解过BigTable/HBase架构那理解MergeTree会更快速底层数据是如何组织的索引如何构建有哪些索引各种“键”有何差别对性能和行为各有什么影响1.1 MergeTree 是核心引擎ClickHouse 最核心的表引擎是 MergeTree 家族。它被设计用来处理海量数据高吞吐写入、大规模列式存储、快速扫描和聚合。MergeTree 的写入模型和 LSM Tree 有相似之处数据先批量进入内存排序后写成一个不可变的 part后台再异步合并多个 part。不同的是ClickHouse 不维护传统意义上的 memTable也不优先优化点写入、点更新或事务恢复而是围绕 OLAP 的批式写入和批式查询做取舍。不维护 memTable 的原因主要是架构复杂性和负载模型ClickHouse 的主场景是 OLAP写入通常是批式或微批。如果引入 memTable会带来 WAL、崩溃恢复、更多内存数据结构、更多写放大和一致性处理。对分析型系统来说简化写入路径、提升批量吞吐通常比优化单行写入更重要。1.2 Part、Partition、Granule、MarkClickHouse 的 MergeTree 表可以从这几个层次理解partitionKey决定数据分区可以近似理解为数据目录常用于生命周期管理比如按天、月分区后直接 drop 旧分区。part每次批量写入后形成的物理数据片段后台 merge 会不断合并 part。granuleClickHouse 的基本读取粒度一组连续行默认index_granularity 8192。mark每个 granule 的索引标记记录该 granule 的主键值和列文件中的位置。因此物理存储如下图所示partition | - parts | - primary.idx (granule file): whats the primary key of start row of each granule? used to skip irrelevent data - mark file (index, column level) - data file (column level)ClickHouse 不会为每一行建立索引而是为每个 granule 建立一条稀疏索引记录。默认情况下primary index 存储每 8192 行中第一行的主键值。查询时ClickHouse 先用主键稀疏索引定位可能相关的 granule再通过每列自己的 mark file 找到对应列数据在磁盘上的物理位置。注意这里每个part里每一列数据会单独存放但是每个granule的数据并不会分开存。只是逻辑上用mark文件可以把每一列的数据切成多个granule分开读取跳过无关数据。这种多层索引设计虽然读/写放大不小但是它的关键收益是primary index 可以足够小并且完整加载到内存。代价是 ClickHouse 的读取粒度不是单行而是一段 granule所以它更适合扫描、过滤和聚合而不是点查和高频小更新。尤其是高频小更新会造成生成太多partmerge不过来。Q: 如果多次写入后有多个part那内存里的primary index是如何组织的是每个part都有自己的primary index还是所有part的primary index合并成一个A 在 ClickHouse 中每个 part 都有自己的 primary index。每个 part 的 primary index 是独立的记录了该 part 中每个 granule 的主键值和位置。当查询时ClickHouse 会加载所有相关 part 的 primary index 到内存中但它们并不会合并成一个单一的索引结构。相反查询引擎会在内存中维护一个列表或集合包含所有相关 part 的 primary index然后根据查询条件逐个检查这些索引以确定哪些 granule 需要被读取。这种设计允许 ClickHouse 高效地处理多个 part同时保持索引的稀疏性和内存占用的低成本。1.3 列式存储和 Wide FormatClickHouse 是列式存储。MergeTree 的数据在磁盘上按 part 组织每个 part 是一个目录在 wide format 下每一列都有自己的数据文件和 mark 文件。列式存储带来的直接收益是查询只读取需要的列。如果 SQL 只 select 3 列就没有必要读取整行里的其他几十列或几百列。同时列式存储也更容易压缩。相同类型、相似分布的数据集中放在一起压缩率通常比行存更高。如果排序键让相似值更接近压缩效果还会进一步提升。1.4 主键、排序键、分片键ClickHouse 里几个 key 的含义容易混淆shard key分片方式类似 Elasticsearch 的 routing key用来决定数据落到哪个 shard。只在分布式模式中才生效partitionKey分区键主要服务生命周期管理和分区裁剪。primary key主键稀疏索引的 key用于加速过滤。sorting key/ORDER BYpart 内的数据排序方式影响过滤效率和压缩率。ClickHouse 的 primary key 不等同于传统数据库里的唯一约束。ClickHouse 允许多行拥有相同的 primary key 值。它的 primary key 更接近“用于跳过数据的索引”。如果同时指定 primary key 和 sorting keyprimary key 必须是 sorting key 的前缀。排序键和主键可以不完全一样但二者共同影响过滤效率、数据局部性和压缩率。1.5 Data Skipping IndexClickHouse 支持 data skipping index可以理解为二级跳数索引用来在主键之外进一步跳过不相关数据。常见的data-skipping index有min-max范围比较聚集布隆过滤器user_id等高基数字段等联想看到这里如果之前对parquet/iceberg有了解的同学应该会感到很熟悉因为这些存储格式也都提供了这些二级索引方便用户跳过无关数据加速查询。大数据领域能减少IO的方式不多可用的索引也不多常见的就是排序但是排序没法作为二级索引、布隆过滤器、minmax等。1.6 Projection、物化视图如果希望针对多种访问模式建立“多个索引视角”通常需要额外的数据组织方式手动复制一份表并使用不同排序键。创建物化视图把数据按另一个查询模式重新写入。使用 projection让 ClickHouse 在同一张表内维护额外的数据布局。本质上这些方案都是用额外存储换查询性能。CH为什么快ClickHouse 快不是来自单一技巧而是存储、执行、索引、压缩、并行化一起配合的结果以下小章节会分点说明。延伸和spark这类离线计算引擎相比CH是常驻服务没有启动时间开销计算也发生在存储节点上没有网络传输开销因此才可能做到亚秒级的响应。这些虽然不是面经里会提到的但我觉得是计算能做到秒级以内的关键因素。因为什么向量化、计算优化spark肯定没少做什么列存、skip indexparquet/iceberg也都有。但是Spark冷启动执行一条SQL是绝不可能做到秒级的因为几秒内spark甚至都还没启动。2.1 列存优势不读无关列列式存储让 ClickHouse 只读取查询需要的列。对宽表分析来说这是非常关键的优化很多查询虽然基于一张大表但实际只用少量字段。2.2 少读行组MergeTree 的 primary index 是稀疏索引。查询会先根据主键范围排除不相关 granule再读取剩下的数据块。它不是精确定位单行而是尽量减少扫描范围。Data skipping index、partition pruning、projection 也都服务于同一个目标尽可能少读数据。2.3 顺序读取和并行扫描ClickHouse 读取的是 granule而不是单行。虽然这牺牲了点查粒度但换来了顺序 IO、批量处理和并行扫描能力。多个列文件、多个 part、多个 shard 都可以并行参与查询。2.4 压缩减少 IO列式数据通常压缩率更好。排序键把相似数据放到一起后压缩效果更好磁盘读取量也更小。对分析查询来说少读磁盘往往比 CPU 多做一点解压更划算。2.5 向量化和近似计算ClickHouse 的执行模型适合批量处理数据块。它还提供大量近似计算能力比如近似去重、分位数、采样等。在业务可以接受近似结果时可以用精度换更低延迟和更少资源。例如近似 distinct count。分位数计算。按部分 key 或采样数据做聚合估算。使用技巧3.1 批量写入不要单行高频写入ClickHouse 不怕大吞吐写入怕小数据量高频写入。每次 insert 都可能产生新的 part如果一行一行写会造成 part 数量过多影响查询和后台 merge。如果上游是流式数据更推荐在上游攒批后微批写入。例如每次至少几千到几万行常见经验是每批 10k 行左右起步再根据数据宽度、延迟要求和集群资源调整。小知识如何解决流式系统的小文件问题流式系统写入时如果配置不当很容易造成小文件问题而大多数OLAP引擎对小文件的支持都不好因为小文件更偏向OLTP负载上游攒一批数据再写入OLAP系统比如Flink每10秒写入一次定期压缩合并小文件这也是merge后台线程在干的事情选择合适的接收系统比如有的系统专门为流式负载设计对并发写入例如表格式中的hudi或者小文件支持较好3.2 控制 Part 数量和大小Part 数量过多会让查询和 merge 压力变大。一个粗略经验单分区 parts 小于 10 通常比较健康。100 以下通常还能接受。500 左右需要关注。大于 1000 通常需要介入。但也不是 part 越大越好。过大的 part 会给后续 merge 带来压力。很多场景下把 part 控制在数百 MB 是比较合适的起点。3.3 合理设计 PartitionPartition 主要用于生命周期管理而不是越细越好。常见做法是按日期、月份或业务生命周期分区。这样可以快速 drop 旧数据也能让查询做 partition pruning。如果 partition 过细会制造大量小 part 和元数据压力。3.4 排序键服务高频过滤和压缩排序键应该优先考虑高频过滤条件、数据局部性和压缩效果。相似数据排在一起可以提升压缩率过滤字段排在前面可以提升跳数效果。如果一张表有多种完全不同的查询路径不要指望一个排序键同时服务所有场景。可以考虑 projection、物化视图或复制表。什么是物化视图就是名义上是个view实际上会将view的逻辑执行一遍存成物理数据集。它和单独计算生成物理数据的差别在于物化视图可以随着数据源的改变、逻辑的改变自动重新生成物理数据集省去了人工管理/定期运行成本3.5 慎用 Nullable尽量选择紧凑的数据类型减少不必要的Nullable。Nullable(T)会额外创建一列UInt8类型的 null 标记。每次处理这个 Nullable 字段时ClickHouse 都需要处理额外列这会增加存储和计算开销。因此如果业务上能用默认值、空字符串、特殊枚举或外部语义表达缺失值可以避免滥用 Nullable。3.6 理解不同 MergeTree 引擎的语义MergeTree 家族的不同引擎核心差异在于 merge 时如何处理相同主键的数据普通MergeTree只合并 part不对相同主键做业务语义处理。ReplacingMergeTree可在 merge 时按版本保留新值。SummingMergeTree可在 merge 时对数值列求和。AggregatingMergeTree可在 merge 时合并聚合状态。这些语义通常发生在后台 merge 阶段不应假设写入后立即完成。3.7 谨慎使用 FINALFINAL会在查询时强制按照引擎语义处理重复或可合并数据。对于普通MergeTree使用FINAL通常没有意义对于ReplacingMergeTree等表它可以保证查询时看到去重或替换后的结果。但FINAL会影响性能尤其可能破坏局部聚合优化。例如计算COUNT时原本可以把多个 part 的 count 相加用了FINAL后需要先做去重或合并再聚合。如果必须保证结果正确可以选择查询时使用FINAL。使用OPTIMIZE FINAL强制触发合并。通过业务语义过滤例如保留version max(version)的记录。4. QA杂项问题Q1ClickHouse 支持物化视图吗支持。ClickHouse 的物化视图常用于把写入数据同步转换到另一张表中也可以用来维护不同排序键、预聚合结果或查询加速表。Q2ClickHouse 适合做数仓吗它可以承担数仓中的存储和查询层但不是完整数仓平台。它缺少内置 DAG、调度、复杂数据开发治理等能力也不擅长复杂大表 Join 和多层依赖 Pipeline。更常见的方式是让 Spark、dbt、Flink 等负责转换和编排ClickHouse 负责高性能查询。Q3ClickHouse 为什么不适合大表 JoinClickHouse 的优势是列式扫描、过滤、聚合和局部并行。大表 Join 往往需要大量 shuffle、内存构建 hash table、跨节点数据移动和复杂执行计划这不是它最擅长的模型。它更适合事实宽表加小维表 join或者提前通过建模把常用字段打宽。Q4ClickHouse 适合 CDC 吗不适合作为 CDC 的原始状态存储或事务型更新系统。ClickHouse 可以接收 CDC 结果但更适合接收已经规整成批或微批的数据。如果要表达更新语义通常需要使用ReplacingMergeTree、版本字段、物化视图或上游合并逻辑。Q5Primary Key 是唯一约束吗不是。ClickHouse 允许相同 primary key 的多行存在。它的 primary key 主要是稀疏索引即排序索引用于加速数据跳过不负责唯一性约束。Q6为什么 primary index 可以放进内存因为它不是每行一条而是每个 granule 一条。默认一个 granule 是 8192 行所以 primary index 的规模远小于总行数。它只记录主键值和定位 granule 所需的信息具体列数据位置再通过每列 mark file 查找。Q7Merge 一定会很快完成吗不一定。ClickHouse 后台 merge 更关注系统整体吞吐不保证某次去重、替换或聚合在固定时间内完成。如果业务依赖 merge 后语义需要使用FINAL、OPTIMIZE FINAL或业务版本过滤保证结果。