ClickHouse 的“独孤九剑”:极速查询的终极秘籍

引言

在大数据时代的江湖,数据量呈爆炸式增长,如何高效地处理和分析海量数据成为了一个关键问题。各路英雄豪杰纷纷亮出自己的绝技,争夺数据处理的巅峰宝座。而在这场激烈的角逐中,ClickHouse 以其"独孤九剑"般的绝世武功,横空出世,令群雄侧目。

ClickHouse 是一个用于联机分析处理(OLAP)的开源分布式数据管理系统。它由俄罗斯的 Yandex 公司开发,为海量数据的实时分析处理提供高效的解决方案。

今天,就让我们一同走进 ClickHouse 的江湖,揭开它极速查询的终极秘籍。

ClickHouse "九剑"

总决式:整体架构

从架构角度来看,数据库至少由存储层和查询处理层组成。存储层负责保存、加载和维护表数据,而查询处理层则执行用户查询。与其他数据库相比,ClickHouse 在这两个层上都提供了创新,可实现极快的插入和选择查询。不同于其他大数据计算引擎(如 Spark、Presto 等)的"计算服务于存储",ClickHouse 自有存储层,不依赖外部存储,可以在存储层为查询计算做出很多的优化特性,即存储服务于计算。

ClickHouse 采用 MPP(大规模并行处理)架构,集群中的每个节点都是对等的,可以独立对外提供服务。这种架构使得 ClickHouse 能够高效地处理分布式查询,通过将任务并行地分散到多个服务器节点上,在每个服务器节点进行计算。

同时,ClickHouse 对主键预排序,数据基于列式存储,并采用向量化引擎等方式保证了 ClickHouse 的快速查询分析。

破剑式:列式存储

列式存储,被广泛应用于大数据领域,Parquet、ORC 等文件格式都是列式存储格式。

在存储引擎的设计上,ClickHouse 同样采用了基于列存储的存储结构。每个列的数据单独存储在一个文件中,这种结构使得读取和过滤数据时可以只访问相关的列,在很多场景中极大地降低了数据分析过程中读取的数据量。

列存储还为 ClickHouse 带来另一个非常明显的优势就是大幅提高了数据压缩率。由于列中的数据通常具有相似的特征,压缩效率更高,从而大幅减少压缩后的数据大小,极大减少了磁盘的 I/O 时间。并且,每一列都可以使用不同的压缩算法。在实际生产中,ClickHouse 基本可以达到 8:1 的压缩比。

在查询过程中,ClickHouse 只解压涉及的列数据块,而不是解压整个数据块,这样可以减少解压缩过程中的 I/O 操作,进一步提高查询效率。

破刀式:向量化

向量化执行不是一次处理一条数据,而是将数据分成批量(通常是固定大小的数据块),然后一次性处理整个批次的数据。例如,一次处理1024个数据元素。

在计算引擎上,ClickHouse 首次使用向量化计算引擎。ClickHouse 利用现代 CPU 提供的 SIMD(单指令多数据)指令集进行向量化加速。SIMD 允许在一个指令中同时对多个数据进行操作,从而显著提高了数据处理的速度。

并且,由于数据是批量处理的,数据的访问模式更具顺序性,减少了 CPU 缓存的未命中率。

因为采用了向量化计算引擎,从而使 ClickHouse 很大程度上提升了单机性能。在实际场景中,上亿乃至几十亿的数据,都可以使用单机解决。符合当前大环境下的降本增效,而且,很大程度上解决了传统大数据仓库的效率低及成本高的问题。

特别地,受 ClickHouse 的影响,Spark、Presto、Doris、StarRocks 等都开始了向量化引擎的改造,像 Spark、Presto 的向量化引擎 Velox,甚至现在连 Flink 都开始搞起来向量化引擎。

破枪式:预排序

ClickHouse 使用类 LSM 的算法,在将数据写入磁盘前进行排序,以保证数据在磁盘上有序。数据在写入后定期在后台进行 Compaction。通过类 LSM Tree 的结构,顺序写磁盘,充分利用了磁盘的吞吐能力。

ClickHouse 在建表时,需要指定主键及排序键。如果只指定排序键,那么主键会被隐式设置为排序键。如果同时指定了主键和排序键,则主键必须是排序键的前缀。

并且,由于数据是有序的,在设计范围查找及排序操作时,可以用来减少磁盘读取的数据量,进而提升查询速度。数据的预排序是 ClickHouse 很多特性的基础。

破鞭式:表引擎

在 ClickHouse 中,表引擎通过各自的设计和优化机制,使得 ClickHouse 能够在不同的场景下实现高效的查询性能,满足各种数据分析需求。

表引擎决定了:

复制代码
数据存储的方式和位置、向何处写入数据以及从何处读取数据;
支持哪些查询,以及如何支持;
并发数据访问;
使用索引(如果有的话);
是否可以执行多线程请求;
数据复制参数;

破索式:数据类型

Clickhouse 支持 100 多种数据类型。

·基本数据类型 ·Bool :布尔类型,在内部存储为 Uint8,true 为 1,false 为 0; ·UInt8, UInt16, UInt32, UInt64 :无符号整数类型,分别占用 1 字节、2 字节、4 字节、8 字节; ·Int8, Int16, Int32, Int64 :有符号整数类型,分别占用 1 字节、2 字节、4 字节、8 字节; ·Float32, Float64 :浮点数类型,分别单精度和双精度,分别占用 4 字节、8字节; ·Decimal32,Decimal64,Decimal128 :高精度类型,原生方式为 Decimal(P, S); ·String :字符串类型,可变长度; ·FixedString(N) :固定长度字符串类型,指定长度为 N,插入的字符串长度少于 N,则补空,对于 N,则报异常; ·日期和时间数据类型 ·Date :日期类型,以 YYYY-MM-DD 格式存储; ·DateTime :日期时间类型,以 YYYY-MM-DD HH:MM:SS 格式存储; ·DateTime64(N) :带有精度 N 的日期时间类型,N 为从 1 到 9 的精度值; ·复杂类型 ·Array(T) :数组类型,包含元素类型 T 的数组; ·Nested :嵌套类型,支持嵌套结构; ·Tuple(T1, T2, ...) :元组类型,包含多个字段,字段类型可以是不同的数据类型; ·Map(K ,V) :键值类型,映射不唯一,即一个映射可以包含两个具有相同键的元素; ·Enum :枚举类型,用来定义常量; ·聚合类型 ·AggregateFunction: 聚合函数类型,具有实现定义的中间状态,可以将其序列化为 AggregateFunction(...) 数据类型并存储在表中,通常使用物化视图来存储。产生聚合函数状态的常用方法是调用带后缀的聚合函数 -State。将来要获得聚合的最终结果,必须使用带 -Merge 后缀的相同聚合函数; ·SimpleAggregateFunction: 产生聚合函数值的常见方法是调用带有 -SimpleState 后缀的聚合函数,如 min、max、sum、groupArrayArray 等。SimpleAggregateFunction 比使用相同的聚合函数具有更好的性能 AggregateFunction; ·其他数据类型 ·UUID :UUID类型,用于存储全局唯一标识符; ·IPv4,IPv6 类型 :IP类型,IPv4 基于 UInt32 封装,IPv6 基于 FixedString(16) 封装,包含格式检查; ·Nullable(T) :可空类型,包装类型 T,允许存储 NULL 值; ·LowCardinality(T) :将其他数据类型的内部表示更改为字典编码,使用字典编码数据进行操作可显著提高许多应用程序的 SELECT 查询性能,一般而言字典包含少于 1 W 个不同的值,则会有更好的读取及存储效率,如果大于 10 W 的不同值,那么相比其他数据类型性能可能会更差;

数据类型决定了数据在内存中的组织形式、在磁盘中存储的持久化和序列化方式以及在计算时的处理机制。

ClickHouse 中的列 (IColumn) 事实上是一个数组,存储某一列的一个或多个数据。ClickHouse 会将列中的数据当成整体进行处理,而不是将列中的数据一行一行地处理。

列是不可变的,任何对列的操作都会产生一个全新的对象。不可变的语义使得某些对列的操作可以并行处理,从而充分利用多核处理器。

在 ClickHouse 中,内存对齐的数据类型只保存数据数组,内存中无法对齐的数据还额外保存了一个用于定界的 offset 数组。

由于内存对齐的数据类型在存储时不需要额外存储数据的边界,在计算时也不需要额外处理数据边界,因此内存对齐的数据具有更高的存储与计算效率。

在列式存储中,每个列的数据单独存储,同一种数据类型的数据,对于数据压缩更加友好,压缩效率更高。(详见《破剑式:列式存储》章节)

数据类型的设计充分考虑了大数据场景下的性能,带来了极高的查询效率,但同时也对使用者提出了更高的要求,使用者必须正确了解数据特点且必须正确使用数据类型,否则可能造成内存浪费、查询缓慢等问题。

破掌式:分片与副本策略

在 ClickHouse 中,为了提升查询性能及增加数据容错性,分别在水平方向和垂直方向上划分为分片(shard)及副本(replica)。

在分布式模式下,ClickHouse 会将数据在水平方向上分为多个分片,并且分布到不同节点上,不同分片间的数据不同。分片的目的主要是为了提升查询性能,方便多线程及分布式查询。在分布式查询时,按照分片数量拆分成若干个对本地表的子查询,然后依次查询每个分片的数据,再合并汇总返回。

在数据写入时,需要考虑如何均匀地写入至各个分片,以及在数据查询时,需要考虑如何路由到每个分片,并计算汇总形成结果集,这是分片就需要结合 Distributed 表引擎一起使用。在集群配置分片规则时,每一个分片会配置一个权重,分片权重会影响数据在分片中的倾斜程度,分片权重越大,被写入的数据越多。在创建 Distributed 表时,需要指定 sharding_key 即分片键,它必须是一个整数类型的值。分片键的分片策略一般有以下几种:

固定字段:按照用于指定的字段的余数进行划分,字段需要是整数类型;

随机函数:按照随机数进行划分;

hash 函数:按照指定字段的 hash 值进行划分;

ClickHouse 会将数据在垂直方向上分为多个副本,即同一个数据可以在不同的节点上存储,并且数据副本间的每份数据相同,通过增加数据存储容易来防止数据丢失。可以在任意一个副本上执行 INSERT 、ALTER 操作,效果是相同的,都会借助 Zookeeper 的协同能力被分发至每个副本以本地形式执行。

在数据查询时,一个表的一个分片会拥有多个副本,那么这时会存在分布式表引擎选择哪个副本计算的问题。ClickHouse 使用负载均衡算法从多个副本中选择一个,算法是由 load_balancing 参数控制,并提供了以下选择副本的算法策略:

Random(默认) :会对每个副本计算错误数,查询将发送到错误最少的副本,如果多个副本同时拥有同样最小错误数,则随机选择一个;

Nearest hostname(最近主机) :系统同样会对每个副本计算错误数,每 5 分钟错误数会除以 2,同样会选择错误最少的副本,如果有多个副本有最小的错误数,则会将查询发送到配置文件中的服务器主机名同当前分布式表所在节点的主机名最相似的副本;

Hostname levenshtein distance(主机名编辑) :类似 Nearest hostname,但它是以编辑距离的方式比较主机名;

In Order(排序) :具有相同错误数的副本将按照配置中指定的顺序进行访问,当确切知道哪个副本更可取时,此方法是合适的。

First or Random(第一个或者随机) :此算法选择集合中第一个副本,如果第一个副本不可用,则随机选择副本。

Round Robin(循环遍历) :此算法在具有相同错误数的副本中使用循环遍历的策略选择。

破箭式:索引设计

在 ClickHouse 中,索引是优化查询性能的关键部分。ClikcHouse 中主要支持是稀疏索引和跳数索引。

在 ClickHouse 中,主键索引采用稀疏索引实现,仅对每个颗粒(index_granularity,默认为 8192 行为一个颗粒)记录一个索引条目(mark),索引条目存储的是颗粒(index_granularity)第一行的主键列值,存储到单独的索引文件(primary.idx),并在标记文件({列}.mrk)存储每个颗粒到文件物理位置的映射。通过索引文件和标记文件,才能共同确定一个数据所在的文件位置。在查询时,首先通过索引确认数据所在的个颗粒,然后依据标记确认个颗粒所在的物理地址,最后通过物理地址从硬盘上读取数据。

这种索引设计允许主键索引很小,必须完全适合加载到内存,同时仍可显著加快查询执行的时间,尤其是对数据分析中常见的范围查找。其核心逻辑是通过索引降低需要读取的数据量,从而减少磁盘 I/O 时间,达到加速查询的效果。

除了主键索引,ClickHouse 还支持跳数索引。跳数索引允许在查询时跳过没有匹配值的数据块,减少扫描数据范围。主要的跳数索引有 minmax、set、布隆过滤器三种类型。

minmax 类型的跳数索引,存储每个块的索引表达式的最大值及最小值。这种类型非常适合倾向于按值松散排序的列,且只能与标量或者元组表达式一起正确工作(永远不会应用于返回数组或 MAP 数据类型的表达式),通常是查询处理过程中成本最低的索引类型。

众所周知,布隆过滤器允许以极小的误差来高效地判断一个元素是否存在于集合中。由于布隆过滤器可以更有效地处理大量离散值的测试,因此适用于产生更多值进行测试的条件表达式,比如数组和 MAP,以及对文本搜索也很多有用,特别是没有单词分隔符的语言(比如中文)。

通常,set 索引和布隆过滤器索引都是无序的,因此不适合用于范围查找。相反,minmax 索引特别适合于范围判断,因为确定范围是否相交是非常快的。

破气式:计算引擎

从功能和整体架构上讲,ClickHouse 的计算引擎与其他数据库的计算引擎并没有很大的不同。功能都是将可描述的结构化查询语言(SQL)转化翻译为可以执行的物理计划以及执行计算并获得计算结果,而整体架构都有包含 SQL 解析、翻译解释、计算执行等部分。

从实现方面,ClickHouse 同样采用了多线程及分布式查询,成为其高性能和高扩展性的关键。通过线程级并行的方式提升性能,利用多核 CPU 的计算能力;通过采用分布式架构,支持分片和副本,通过将数据分布到多个节点实现存储和计算的扩展。

特别地,ClickHouse 的计算引擎是相当被诟病的。ClickHouse 没有成熟的执行计划优化器,并且对 JOIN 的支持相比其他数据库及计算引擎相当薄弱。所以,有人认为 ClickHouse 的计算引擎缺乏优化及对分布式的支持,就是个半成品。

最后

通过上述特点,我们可以清楚地看到,ClickHouse 的高性能查询能力并非偶然,而是其架构设计和技术创新的必然结果。无论是列式存储、向量化执行引擎,还是数据压缩技术、分布式架构,这些特点共同作用,使得 ClickHouse 在处理大规模数据分析时表现出色,在大数据的江湖中独步天下。

相关推荐
·薯条大王2 小时前
MySQL联合查询
数据库·mysql
morris1314 小时前
【redis】redis实现分布式锁
数据库·redis·缓存·分布式锁
hycccccch5 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq
这个懒人5 小时前
深入解析Translog机制:Elasticsearch的数据守护者
数据库·elasticsearch·nosql·translog
Yan-英杰5 小时前
【百日精通JAVA | SQL篇 | 第二篇】数据库操作
服务器·数据库·sql
NineData6 小时前
NineData云原生智能数据管理平台新功能发布|2025年3月版
数据库
百代繁华一朝都-绮罗生7 小时前
检查是否存在占用内存过大的SQL
数据库·sql
吾日三省吾码7 小时前
Python 脚本:自动化你的日常任务
数据库·python·自动化
CZIDC7 小时前
win11 系统环境下 新安装 WSL ubuntu + ssh + gnome 桌面环境
数据库·ubuntu·ssh
直裾7 小时前
Mapreduce的使用
大数据·数据库·mapreduce