当前位置: 首页 > news >正文

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

clickhouse(以下简称CH)是互联网企业常见的OLAP引擎。因此我抱着好奇的态度开始学习CH。这篇笔记的知识来源于CH的官网和AI对话,如果大家想系统性地学习CH,那从CH官方文档开始是最合适的,免费且系统(我尝试过找CH的书籍,没有找到很权威的),甚至还有中文版,还有详尽的示意图。如果对某个特定的知识点有疑问,那可以用AI进行深入的追问。

当我希望开始接触CH时,脑海里大概是这些问题,希望大家也可以有自己的问题,带着问题去学习CH

  • CH在数据系统中的地位是什么?计算有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、Mark

ClickHouse 的 MergeTree 表可以从这几个层次理解:

  • partitionKey:决定数据分区,可以近似理解为数据目录,常用于生命周期管理,比如按天、月分区后直接 drop 旧分区。
  • part每次批量写入后形成的物理数据片段,后台 merge 会不断合并 part。
  • granule:ClickHouse 的基本读取粒度,一组连续行,默认index_granularity = 8192
  • mark:每个 granule 的索引标记,记录该 granule 的主键值和列文件中的位置。

因此物理存储如下图所示:

partition | - parts | - primary.idx (granule file): what's 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,所以它更适合扫描、过滤和聚合,而不是点查和高频小更新。尤其是高频小更新会造成生成太多part,merge不过来。

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 Format

ClickHouse 是列式存储。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 BY:part 内的数据排序方式,影响过滤效率和压缩率。

ClickHouse 的 primary key 不等同于传统数据库里的唯一约束。ClickHouse 允许多行拥有相同的 primary key 值。它的 primary key 更接近“用于跳过数据的索引”。

如果同时指定 primary key 和 sorting key,primary key 必须是 sorting key 的前缀。排序键和主键可以不完全一样,但二者共同影响过滤效率、数据局部性和压缩率。

1.5 Data Skipping Index

ClickHouse 支持 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 index,parquet/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 合理设计 Partition

Partition 主要用于生命周期管理,而不是越细越好。常见做法是按日期、月份或业务生命周期分区。这样可以快速 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 谨慎使用 FINAL

FINAL会在查询时强制按照引擎语义处理重复或可合并数据。对于普通MergeTree,使用FINAL通常没有意义;对于ReplacingMergeTree等表,它可以保证查询时看到去重或替换后的结果。

FINAL会影响性能,尤其可能破坏局部聚合优化。例如计算COUNT时,原本可以把多个 part 的 count 相加;用了FINAL后,需要先做去重或合并,再聚合。

如果必须保证结果正确,可以选择:

  • 查询时使用FINAL
  • 使用OPTIMIZE FINAL强制触发合并。
  • 通过业务语义过滤,例如保留version = max(version)的记录。

4. QA:杂项问题

Q1:ClickHouse 支持物化视图吗?

支持。ClickHouse 的物化视图常用于把写入数据同步转换到另一张表中,也可以用来维护不同排序键、预聚合结果或查询加速表。

Q2:ClickHouse 适合做数仓吗?

它可以承担数仓中的存储和查询层,但不是完整数仓平台。它缺少内置 DAG、调度、复杂数据开发治理等能力,也不擅长复杂大表 Join 和多层依赖 Pipeline。更常见的方式是让 Spark、dbt、Flink 等负责转换和编排,ClickHouse 负责高性能查询。

Q3:ClickHouse 为什么不适合大表 Join?

ClickHouse 的优势是列式扫描、过滤、聚合和局部并行。大表 Join 往往需要大量 shuffle、内存构建 hash table、跨节点数据移动和复杂执行计划,这不是它最擅长的模型。它更适合事实宽表加小维表 join,或者提前通过建模把常用字段打宽。

Q4:ClickHouse 适合 CDC 吗?

不适合作为 CDC 的原始状态存储或事务型更新系统。ClickHouse 可以接收 CDC 结果,但更适合接收已经规整成批或微批的数据。如果要表达更新语义,通常需要使用ReplacingMergeTree、版本字段、物化视图或上游合并逻辑。

Q5:Primary Key 是唯一约束吗?

不是。ClickHouse 允许相同 primary key 的多行存在。它的 primary key 主要是稀疏索引(即排序索引),用于加速数据跳过,不负责唯一性约束。

Q6:为什么 primary index 可以放进内存?

因为它不是每行一条,而是每个 granule 一条。默认一个 granule 是 8192 行,所以 primary index 的规模远小于总行数。它只记录主键值和定位 granule 所需的信息,具体列数据位置再通过每列 mark file 查找。

Q7:Merge 一定会很快完成吗?

不一定。ClickHouse 后台 merge 更关注系统整体吞吐,不保证某次去重、替换或聚合在固定时间内完成。如果业务依赖 merge 后语义,需要使用FINALOPTIMIZE FINAL或业务版本过滤保证结果。

http://www.cnnetsun.cn/news/3196090.html

相关文章:

  • Camera Shakify:用真实运动数据为Blender相机注入生命力的插件
  • 05_C++数组
  • 2026年了,还在优化页面不优化段落?你out了!
  • 15个实用的API测试工具,命令行与GUI
  • 在线教育平台|网络学习平台|在线培训系统系统源码在线学习教育平台|JavaWeb | vue|SpringBoot|前后端分离| 项目并附带万字文档(源码+数据库+万字详设文档+软件包)
  • 基于STM32单片机烟雾温度防盗报警 物联网云平台 火灾检测系统312(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_
  • 钢结构厂房各部位名称及代号
  • Nova视觉小说框架:程序员的高效Unity视觉小说开发解决方案
  • 如何使用Conda国内源安装虚拟环境(保姆级全平台教程)
  • Postman进阶指南:电商API测试自动化与持续集成实践
  • 0x03 RLPD 训练范式全解析
  • YOLOv8船舶检测实战:从算法原理到海事场景部署
  • 豆包、千问同步下线智能体!自建AI自动化工作流迁移指南
  • 英雄联盟Akari助手:终极免费开源游戏效率提升工具完整指南
  • 十七、在k8s中监控mysql
  • 视频号批量下载终极指南:3分钟搞定全网热门资源采集
  • Plone开发四条铁律:ZODB、Generic Setup、Volto Block与安全默认
  • Node.js SQLite数据库加密实战:SQLCipher集成与安全存储指南
  • 文件上传漏洞实战解析:从Webshell到服务器控制的攻防对抗
  • Java实现文件异或加密:轻量级本地配置文件保护方案
  • 2026年7月宁波小程序开发公司哪家好?定制开发推荐与排行参考
  • WeblogicScan工具详解:自动化检测WebLogic历史漏洞的原理与实践
  • MySQL 8.0 图书管理系统数据库设计:从 E-R 图到 10 张表的性能优化实践
  • LRFMC模型 Python 3.12 实战:5步完成航空公司客户价值聚类与雷达图可视化
  • 用友NC系统安全深度剖析:SQL注入、XXE与控制台绕过的组合攻击链与加固实战
  • 外文论文辅导平台哪家好?2026年主流服务对比与选择指南
  • AI 开源工具链复现:环境锁定比安装成功更重要
  • Python取模运算深度解析:从循环索引到负数余数的工程实践
  • Web自动化测试中验证码处理与Cookie管理的实战策略
  • ntpd vs chrony vs systemd-timesyncd:3 款 Linux 时间同步方案对比与选型指南