ClickHouse 是一款 ROLAP 列式数据库,在海量数据分析场景中,能够帮助我们快速得到想要的"分析性"数据。本文主要从个人视角讲解 ClickHouse 一次数据查询的整体流程,更多的是自己的一些理解和思考,如有不对,欢迎指出和交流。
一个 ClickHouse 集群是通过分片组成。ClickHouse 分片可以由一台或者多台机器构成,当多台机器组成一个分片时,其中一个节点为主副本节点,其余则为数据副本节点,比如上图,副本数则为 1。一个 ClickHouse 集群可以由多台机器构成,当然也可以根据不同业务特性进行划分,多个集群,但每个集群都有少量机器。
ClickHouse 分片你可以理解为就是 ClickHouse 一个单机数据库实例(副本节点也算),多个这种单机数据库实例构成一个 ClickHouse 集群。分片是指包含数据不同部分的服务器(要读取所有数据,必须访问所有分片)。ClickHouse 通过分片,将一张表的数据水平分割在不同的节点上,随着业务的发展,当表数据的大小增加到很大时,也能够通过水平扩容, 保证数据的存储。
副本则是存储复制数据的服务器(要读取所有数据,访问任一副本上的数据即可)。ClickHouse 通过副本节点进行数据冗余存储,用空间来换取数据可用性,当主副本节点不可用时,能够选择其他副本节点进行数据服务。
MergeTree 表引擎数据组织形式从单个分片视角来看,底层通过目录 + 文件的方式进行组织。首先,ClickHouse 会有数据根目录,假设数据根目录为:
/data/clickhouse/data
现在用户创建了一个数据库为 lake 的数据库,那么在这个根目录创建一个为 lake
的目录:
/data/clickhouse/data/lake
当有多个数据库时,底层数据目录结构为:
clichouse根目录路径/数据库名称
此时在 lake
数据库中创建一个名为 hello_lake 的数据表,分区键为日期键(形式为YYYYMMDD),当插入第一批次数据后,假设插入的数据都属于 20210323 这个分区,那么底层表的数据目录结构为:
/data/clickhouse/data/lake/hello_lake/20210323_1_1_0
ClickHouse 每插入一批次数据,则会在底层形成一个或者多个分区目录,具体看插入数据中是否有多个分区的数据。假设现在有插入了一批 20210323 这个分区的数据,那么底层会多一个目录, 2021032322_0,分区相同,但是分区后面的数字不同。关于分区后面数字的解析,具体细节,可以参见《ClickHouse 原理解析与应用实践》6.2.2 章节。
在分区目录中,就是存放的具体数据,分区目录中有这些文件:
primary.idx 是索引文件,会按照你创建表时,指定的 primary key 排序(如果不指定,默认和 order by key 相同),[Column].bin 为列压缩后数据,[Column]表示列的名称,当有多个列时,就会有多个这样的文件。[Column].mrk 表示索引编号与 bin 文件中便宜量的映射。
在生产环境中,业务方在使用 ClickHouse 时,一般会使用到两种类型的表:
数据的写入是直接写入到各个分片上面的本地表,本地表用来真正数据存储,Distributed 表引擎的表则用于数据的查询,Distributed 表引擎本身不存储数据, 但可以在多个服务器(分片)上进行分布式查询。简单理解,Distributed 表引擎只是你真实数据表(本地表)的代理,在进行数据查询时,它会将查询请求发送到各个分片上,结合索引(如果有),并行进行查询计算,最终将结果进行合并,返回到 Client。
下面是一次对于 Distributed 表引擎类型表查询的流程:
整体上查询过程分为 6 个步骤:
在分片上执行查询语句时,会根据查询语句中的分区范围,先进行分区级别的数据过滤。之后在满足分区条件的目录中,通过 primary.idx 文件,结合索引键的取值范围,查询出索引编号的范围,然后通过查询列的 [Column].mrk 文件,找到其 [Column].bin 文件中的偏移量对应关系,最终将数据加载到内存进行分析和计算。
在对 Distributed 表引擎类型表进行查询时,一定要注意子查询的使用姿势,比如下面的语句:
select A,B,C from Y where H = 100 and
Z in (select Z from Y where D in ('a', 'b') )
其中 Y 是 Distributed 表引擎的表,假设 ClickHouse 集群有 10 台机器,那么上面的语句最终会有 100 次查询(N的平方,N 为集群分片数),可能会使查询时间变长,这里可以用 GLOBAL IN 来减少查询次数来进行代替,当然,GLOBAL IN 语法会存在网络数据的分发,同时数据会以临时的内存表存在内存中,所以子查询中的数据不宜过大,如果表数据中存在重复数据的话,也可以通过 Distinct 进行数据去重。