如果你现在就有一张clickhouse
的表,可以用show create table
来查看表的创建。这样看比较直观,如果没有的话,可以按如下的方法进行创建。
MergeTree
的创建方式的完整的语法如下:
CREATE TABLE [IF NOT EXISTS] [db_name.]table_name (
name1[type] [DEFAULT|MATERIALIZED|ALIAS expr],
name2[type] [DEFAULT|MATERIALIZED|ALIAS expr],
....
) ENGINE = MergeTree()
[PARTITION BY expr]
[ORDER BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[SETTING name=value, ...]
...
MergeTree
表引擎除了常规参数之外,该有一些独有的配置。
PARTITION BY
:设置分区键,指定表按什么进行分区。ORDER BY
: 排序键,默认情况下其与主键相同。PRIMARY KEY
: 主键SAMPLE BY
:抽样,声明按何种方式进行采样。SETTING
:对一些常用的clickhouse
参数进行设置。e.g. index_granularity.目录可以看如下的图:
现在简单说一下图中 的含义:
partition
:分区目录。checksum.txt
:校验文件,使用二进制格式存储。columns.txt
:列信息文件,使用铭文存储。count.txt
:计数文件。primary.idx
:一级索引文件,使用二进制格式存储。用于存放稀疏索引。[Column.bin]
:列的数据压缩文件,同时也体现了clickhouse的列存储特性[Column.mrk]
:列字段标记文件,将索引与压缩数据文件联系起来。[Column.mrk2]
:自适应的索引。下面主要介绍一下数据分区、一级索引、数据存储、数据标记。这边用一张图进行一个简单的概述。
需要注意的是MergeTree
不能依靠分区的特性,将一张表的数据分布到多个clickhouse的服务节点。
分区的规则按照设置的分区ID进行区分。简单来说,clickhouse可以按照用户的设定来对数据的分区。分区的ID生成有四种规则:
all
这个分区。UINT64
)YYYYMMDD
进行分区名设定。上面讲完分区规则,那么疑问来了。
clickhouse是怎么对分区进行命名的呢?
这边可以进入clickhouse的文件目录看一下
/var/lib/clickhouse/data
通过文件夹的文件可以看到文件的命名方式如下:
202106-1-2-0
level
表示分区的合并次数,这边出现了合并次数的说法。下面将详细说明。注意:这边的BLOCK按笔者想法是每次一批数据写入就进行BLOCKNUM++的操作。这边的BLOCKNUM是一个全局的变量。
假设:我们按照三个批次进行数据插入操作。具体流程如下:
分区号/时间顺序及操作 | 1 | 2(合并) | 3(合并) |
---|---|---|---|
插入一批数据 | 202105_1_1_0 | 202105_1_2_1 | |
插入一批数据 | 202105_2_2_0 | 202105_1_4_2 | |
插入一批数据 | 202105_4_4_0 | ||
插入一批数据 | 202106_3_3_0 |
可以看到最后的数据为202106_3_3_0和202105_1_4_2。
注意:clickhouse出于性能考虑,在生成新的分区(active=1)时保留旧的分区(active=0)
查询时active=0的数据会自动跳过。
这里由于工作不涉及二级缓存,这边就不做介绍。具体可以看其他博主的博文。
一级索引的主键定义主要由PRIMARY KEY
指定,没有指定的话就按照ORDER BY
指定。这里主要介绍索引的索引粒度(index_granularity)以及索引的生成规则。
一级索引采用稀疏索引,这就好比分页不会具体到某一行数据。
索引粒度这个参数之前提及过,其默认值为8192,clickhouse中提供了自适应调整策略。
对于一组数据,markrange
被用来表示具体的区间,类似于现在数组的下标。
Mark 0 | Mark 1 | Mark 2 | … | Mark N |
---|---|---|---|---|
Markrange(0,1) | Markrange(1,2) | Markrange(2,3) | … | Markrange(N,N+1) |
8192(size) | 8192 | 8192 | … | 8192 |
0~8192(data range) | 8192~16384 | 16384~24576 | … | N*8192~(N+1)*8192 |
由上表可以清晰的看出数据按照索引粒度分割为多个小的区间,每个区间最多index_granularity个数据。一级索引同时也会影响.bin与.mrk的数据。因为光靠一级索引无法完成数据的查询操作。
讲到这个地方可能开始晕了,这个时候可以看看一开始的图缓一缓。
在讲索引的查询过程前,先讲一下索引的生成规则。
假设我们有一张成绩的表,其中的数据按照grade
/grade+date
进行分区。
下图展示了按照这两种方式生成索引的构成。
下面介绍一下查询的过程。具体思想可以描述为交集+剪枝的过程:
Step 1:生成查询的区间,首先将查询条件转化为区间形式
WHERE grade=60 ----> [60,60] #突然想到:需要注意的是clickhouse是大小写敏感的,所以要特别注意大小写的区分
WHERE grade >40 -----> (40,+inf)
Step 2: 递归求交集判断(这里参照分治递归的思想)
end-start>8
)则对查询的MarkRange数值区间进行拆分,重复此过程end-start<8
),则记录markrange进行返回Step 3:合并MarkRange区间
这里的8不是固定值,可以通过设置merge_tree_coarse_index_granularity进行修改。
clickhouse使用列存储,各列独立存储。
在存储数据的方式上面,clickhouse不是一股脑把数据存入bin文件,而是通过压缩的方式进行存储。当前的压缩支持LZ4、ZSTD、MULTIPLE和DELTA几种,默认使用LZ4算法。所有数据会事先按照ORDER BY
进行排序。
bin中存在多个压缩块。
一个压缩数据块由header和body组成,如下图所示:
其中,压缩方法:
- LZ4:0x82
- ZSTD:0x90
- Multiple:0x91
- Delta:0x92
下面讲一下MergeTree的数据写入过程。
如果一批数据(index_granularity)进行写入遵循一下的规则:
size>=64KB
,生成下个数据块。数据标记主要将索引与数据压缩块的数据进行关联。这些信息被记载在.mrk文件中。对应方式如下图:
跟一级索引不同的是:mrk不能常驻内存,使用LRU的方式加快取用速度。
需要注意的是,压缩文件中的偏移位置的大小=压缩块大小+头部的字节数(8字节)
数据标记的工作方式:MergeTree在读取数据时,必须通过标记数据的位置信息才可以找到所需要的数据。整个过程分为读取压缩数据块和读取数据两个过程。
index_granularity
来加载特定的一小段。这也是需要mrk中压缩块中偏移的数据。分区、索引、标记、压缩好比一件艺术品。分解成一块块看似平平无奇,整合起来就可以惊现他人。具体的过程可以根据《Clickhouse 原理解析与应用实践》来学习。