从ClickHouse底层原理,看透数据查询性能的本质
认知冲突

你是不是背惯了各种MySQL调优的八股文?
每当查询慢的时候,你的第一反应是不是:“我是不是该加个联合索引?”“是不是违背了最左匹配原则?”或者在大半夜盯着慢查询日志怀疑人生,觉得是自己SQL写得太烂了?
有没有想过,在“提升查询性能”这件事情上,可能根本不是你技术不行,而是你一开始就选错了赛道?
我们大多数人,习惯了用“战术上的勤奋”去掩盖“战略上的懒惰”。我们习惯了用MySQL去解决所有问题,却很少去想,在海量数据分析这个场景下,为什么有的数据库能比MySQL快上100倍甚至1000倍?
今天,我不教你背八股文,也不聊那些虚头巴脑的概念。我想带你从底层拆解一下ClickHouse这个“性能怪兽”。
我们要做的,不是吹捧一个工具,而是通过对比MySQL和ClickHouse,把“高性能查询”这件事,从黑盒变成白盒。我们要从ClickHouse那些极其变态的底层设计里,提炼出一套通用的、能指导我们架构设计的底层心法。
点赞,关注,ClickHouse走起!
第一层逻辑:存储结构的降维打击

首先,我们要解决的第一个瓶颈,永远是 IO,也就是怎么把数据从磁盘读到内存里。这里我们必须先搞清楚一个核心概念,那就是“行式存储”与“列式存储”在物理层面到底有什么区别。
大家熟悉的MySQL,是典型的行式存储。
在物理磁盘上,一行数据的所有字段是连续存放的。比如有一张表,字段是ID、姓名、年龄。在磁盘文件里,它的排列方式是:先存ID为1的那个人,紧接着存他的姓名张三,再存他的年龄25岁。存完这一整行,再紧接着存第二行。
这种设计是为了“写”方便,因为写入一行数据时,磁头只要追加一次就行,非常适合事务处理。
但是,ClickHouse采用的是列式存储。这并不是ClickHouse独创的技术,它是所有OLAP,也就是分析型数据库的标配。
在列式存储中,数据是把“同类项”合并在一起的。磁盘上会有一个文件专门存所有的“ID”;另一个文件专门存所有的“姓名”;再一个文件专门存所有的“年龄”。
为什么要这么做?
在分析场景下,我们通常面对的是“大宽表”,一张表可能有几百列。但你写SQL分析时,比如计算“用户的平均年龄”,你其实只需要“年龄”这一列。
如果是MySQL这种行式存储,为了拿到年龄,你必须把每一行里包含的“姓名、地址、备注”等几百个无关字段,统统读进内存,然后再剔除。这就导致了严重的读放大。
而ClickHouse这种列式存储,你想查“年龄”,它就只去磁盘读“年龄”这一列的文件。其他几百列的数据,它看都不看。
这时候我们再打个比方:
这就好比你去超市买一瓶水。
在行式存储的规则下,就像在MySQL里,你必须把这瓶水所在的那个货架上所有的商品——毛巾、脸盆、拖鞋——全部打包买回家,才能拿到那瓶水。
而在列式存储的规则下,也就是ClickHouse里,你想买水,我就只给你水。
这一招,直接把IO开销降低了90%以上。
而且,列式存储还带来了一个巨大的连带红利,那就是极致的数据压缩。
大家想,在行存里,一行数据混合了字符串、数字、时间,数据类型杂乱无章,很难找到通用的压缩规律。
但在列存里,这一列全是“时间”,或者全是“整型”。数据类型统一,就能用统一的压缩方式来压缩整列的数据。ClickHouse就是针对不同列的数据类型,会自动选择最合适的压缩算法。
更小的数据意味着读取更快,也意味着同等大小的内存,能存放更多的数据。
所以,优化的第一条铁律:高性能的前提,是物理层面的IO剪枝。不读无用的数据,就是最快的查询。
第二层逻辑:计算能力的极致压榨

好,现在我们通过列存和压缩,把数据高效地读进内存了。下一步拼的是什么?是 CPU 的计算效率。
这里我要讲一个很多开发者容易出现逻辑断层的地方:数据在内存里是什么样子的?CPU又是怎么处理它的?
在MySQL的传统执行器里,处理数据通常是一行一行地迭代。CPU读取第一行,解析,判断where条件;再读取第二行……
这种方式会导致CPU的分支预测经常失败,流水线不断被打断。
ClickHouse为了解决这个问题,引入了两个核心概念:一个是 Block,也就是数据块;另一个是 SIMD,也就是向量化执行。
首先,什么是 Block?
Block是ClickHouse在内存中处理数据的基本单元。你可以把它理解为内存中的一个“微型列存表”。一个Block对象里,包含了三样东西:数据对象、数据类型和列名称。
最关键的是,Block里的数据不是一行一行散落的,而是以数组的形式紧凑排列的。比如一个Block里可能包含了8192个连续的整数。
有了Block这个“方阵”,才能利用CPU的大杀器——SIMD。
SIMD的全称是 Single Instruction Multiple Data,翻译过来就是“单指令多数据流”。这是现代CPU硬件层面的一种能力。
在没有SIMD的时候,在计算机科学里我们管它叫“标量计算”,CPU做一个加法指令,一次只能算两个数的和。
但是有了SIMD,CPU的一个指令,可以同时对一组数据,比如128位或256位寄存器里的数据,进行操作。这种能力极大的提升了CPU的并行计算能力,有点类似于把CPU当GPU来用。很多现代应用都在积极采用SIMD指令来提升程序执行能力。例如Java在新版本中也开始引入了SIMD的支持。
ClickHouse的执行引擎是怎么做的呢?
它不再是一行行处理,而是按Block进行“批处理”。它把Block里的这8192个数据,直接塞给CPU的SIMD寄存器。
这时候,我们再来打个比方:
以前处理数据,就像是你拿着螺丝刀,一个一个地把螺丝拧进去,这就叫标量计算。
现在有了Block构建的数据方阵,配合SIMD指令,就像是你手里拿了一把加特林,或者一个巨大的印章。咔嚓一下,一个指令下去,一批数据的计算全完成了,这就叫向量计算。
这不仅大幅减少了函数调用的次数,更重要的是,它让数据在CPU的L1和L2缓存里待得非常舒服,极大地降低了缓存未命中的概率。
大家记住,现代CPU非常快,但内存相对很慢。如果CPU总是要等待内存把数据送过来,那性能就废了。ClickHouse通过 Block构建数据方阵,配合 SIMD向量化打击,这才是它计算速度快得离谱的真正原因。
第三层逻辑:架构设计的哲学

聊完了存储和计算,最后我们看看架构层面。ClickHouse在宏观设计上,也有两个非常反直觉的取舍。
第一个是:数据有序存储与稀疏索引。
MySQL我们都很熟悉,用的是B+树索引。这是一种“稠密索引”,它试图为每一行数据建立索引。这非常适合查找单个ID,但在分析大量数据时,B+树会导致大量的随机IO。
ClickHouse的MergeTree引擎,强制要求数据在写入磁盘前,必须按照一个指定的键进行物理排序,这个键我们称为 Sort Key。
因为数据已经排好序了,ClickHouse就不需要给每一行建索引了,它只需要每隔几千行建一个标记,这也就是我们常说的稀疏索引。
当你进行范围查询时,命中的数据在磁盘上是紧密连续的。
它把慢得要死的随机IO,变成了飞快的顺序IO。在机械硬盘时代,顺序IO比随机IO快几十倍;即使在SSD时代,顺序读依然有巨大的优势。
第二个是:去中心化的多主架构。
很多分布式系统,像HBase、Spark,都有一个Master节点来统筹全局。一旦Master挂了,整个集群就瘫痪了。
但ClickHouse采用的是 Multi-Master,也就是多主架构。
集群里每个节点角色是对等的。没有所谓的“主控节点”,客户端访问任意一个节点,效果都一样。
这种设计不仅规避了单点故障,更重要的是它把“怎么分数据”的权力交给了开发者。
ClickHouse提供了“本地表”和“分布式表”的概念。
分布式表本身不存数据,它只是一个代理,负责把你的查询请求,“扇出”到各个分片的本地表上,让所有节点并行计算,最后再把结果汇总。
这种Shared-Nothing的架构,让ClickHouse拥有了近乎线性的横向扩展能力。
从数据库到人生选择
讲了这么多,我们总结一下。ClickHouse之所以快,是因为它在三个层面做到了极致:

第一,在存储层,用列式存储和数据压缩,解决IO瓶颈。 第二,在计算层,用Block结构和SIMD指令,解决CPU瓶颈。 第三,在架构层,用有序存储和多主架构,解决磁盘和分布式的效率问题。
最后,我想聊两句题外话。

研究完ClickHouse的底层原理,我最大的感触其实不是它有多快,而是它有多“决绝”。
ClickHouse为了极致的分析速度,它牺牲了很多东西:
它不支持完整的事务,它不擅长高频的单行更新和删除,它的并发能力其实不如MySQL。
它非常清楚自己的定位:我就是为了分析海量数据而生的,其他的,我不在乎。
反观MySQL,它试图做一个“全能选手”,既要管事务,又要管查询,结果在海量数据分析面前,负重前行,步履维艰。
技术架构如此,人生其实也是如此。
我们很多时候焦虑,是因为我们想做MySQL。我们想面面俱到,想在职场上左右逢源,想在技术上全栈通吃,想在生活里滴水不漏。我们试图维护人生这张“大宽表”里每一列的完整性,结果把自己搞得疲惫不堪,性能低下。
但真正的高手,往往是ClickHouse。
他们懂得“列式存储”——只保留核心维度的竞争力,把无关紧要的社交、情绪、琐事统统压缩、甚至丢弃。
他们懂得“向量化执行”——在认准的赛道上,集中所有资源,进行高密度的批量输出,而不是在低效的碎片化事务中空耗CPU。
敢于放弃“全能”的幻想,敢于在某一个维度上做到极致的偏科,这或许才是我们在大数据时代,最该拥有的“高性能”活法。
我是楼兰,关注我,带你看透技术的本质。