clickHouse MergeTree核心原理

1、MergeTree的创建方式与存储结构

1.1 MergeTree的创建方式

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2],
    ...
    INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
    INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2
) ENGINE = MergeTree()
-- 排序键【必填】 默认情况下主键与排序键相同,可以是单个列,也可以通过元组声明多个列
ORDER BY expr
-- 分区键【选填】 可以时单个列,也可以通过元组声明多个列字段,同事也支持列表达式;如果不声明则会生成名为all的分区
[PARTITION BY expr]
-- 主键【选填】 会按照主键字段生成一级索引,默认情况下通常只需要声明排序键代为指定主键,MergeTree允许主键重复【ReplacingMergeTree可以去重】
[PRIMARY KEY expr]
-- 【选填】 声明数据按照何种方式进行抽样,如果声明了则主键中也需要声明同样的表达式
[SAMPLE BY expr]
[TTL expr [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'], ...]
-- index_granularity=8192 表示索引的粒度,表示每隔8192行生成一条索引
-- index_granularity_bytes=10*1024*1024 表示根据一批次写入数据量大小,自适应索引生成间隔大小,这里表示每写入10M就生成一个索引
-- enable_mixed_granularity_parts 表示是否开启自适应索引间隔功能,默认开启
-- merge_with_ttl_timeout 数据ttl功能
-- storage_policy 多路径存储策略
[SETTINGS name=value, ...]

1.2 MergeTree的存储结构

table_name
   |
   |----partition_1  //分区目录余下的各类数据文件都是以分区形式存放的,属于相同分区的数据,最终会被合并到同一个分区目录
   |        |
   |        |-------checksums.txt  //校验文件,二进制存储。保存了余下各类文件的size大小和size的哈希值,校验文件的完整性和正确性
   |        |
   |        |-------columns.txt  //列文件信息,明文存储,保存分区下的列字段信息
   |        |
   |        |-------count.txt  //记录当前分区瞎的数据的总行数
   |        |
   |        |-------primary.idx  //一级索引文件,二进制存储,在数据查询的时候能排除主键条件范围之外的数据文件。
   |        |
   |        |-------[Column].bin  //数据文件,压缩格式存储,默认为LZ4压缩格式。MergeTree采用列式存储,所以每一个文件都有一个.bin数据文件。
   |        |
   |        |-------[Column].mrk  //列字段标记文件,二进制存储,标记文件中保存了.bin文件中的偏移信息。标记文件与稀疏索引文件一一对应,查询时先通过稀疏索引文件找到对应的数据偏移信息(.mrk),再通过偏移量直接从.bin文件中读取数。
   |        |
   |        |-------[Column].mrk2  //当使用了自适应大小的索引间隔,则标记文件会以.mrk2命名。工作原理与.mrk文件相同
   |        |
   |        |-------partition.dat  //如果使用了分区键,则会额外生成partition.dat与minmax索引文件,均使用二进制格式文件。保存当前分区下分区表达式最终生成的值。
   |        |
   |        |-------minmax_[Column].idx  //minmax索引记录当前分区下分区字段对应的原始字段的最大和最小值
   |        |
   |        |-------skip_idx_[Column].idx  //如果在表中声明了耳机索引,则会额外生成耳机索引与标记文件,使用二进制存储
   |        |
   |        |-------skip_idx_[Column].mrk
   |        
   |----partition_2
   |
   |----partition_2

1.3 数据分区

数据分区:针对本地数据而言,对数据的纵向切分

数据分片:针对所有分片上的数据而言,是对数据的横向切分

1.3.1数据分区规则

  • 不指定分区键,则分区默认为all,所有的数据都会写进这个分区。
  • 整形,则分区ID的取值为指定的整形的值
  • 日期类型或者能转换为YYYYMMDD格式的整形,则直接按照整形的字符串数据,作为分区ID的取值
  • 其他类型:当为String Float类型,则通过128位hash值作为分区ID的取值
  • 当分区键为元组的多个列时,分区ID按照上面的方式生成,多个分区ID之间通过“-”符号拼接。

1.3.2分区目录的命名规则

比如分区ID:202103_1_1_0   => {PartitionID}_{MinBlockNum}_{MaxBlockNum}_{Level}

PartitionID: 分区ID,用户创建表时指定的。

MinBlockNum:最小的数据块编号,这是一个整形的全局的自增长编号,当每创建一个分区目录时,计数就累加1。

MaxBlockNum:最大的数据块编号。和MinBlockNum一样当分区创建的时候,MinBlockNum=MaxBlockNum的值

Level:合并的层级,某个分区被合并过的次数,不是全局累加的,对每一个创建的分区,其初始值均为0。之后,以分区为单位,如果相同的分区发生合并动作,则在相应的分区内计数累加1

1.3.3分区目录合并过程

MergeTree会随着每一批次数据写入(一次insert语句)会成成一批新的分区目录,目录状态是激活(active=1)。即便不同批次写入的数据属于相同分区,也会生成不同的分区目录。所以对于同一个分区而言会存在多个分区目录的情况。在写入之后的10~15分钟(默认是)会通过后台任务将数据同一分区的多个数据目录合并成一个目录。已经存在的旧分区目录不会立即删除,目录状态是未激活(active=0),会在之后的8分钟(默认是)通过后台任务被删除。

合并的时候MinBlockNum、MaxBlockNumLevel的值变更

MinBlockNum:取同一分区中所有的目录中最小的MinBlockNum的值

MaxBlockNum:取同一分区中所有的目录中最大的MaxBlockNum的值

Level:取同一分区内所有目录中最大的Level值加1

2、一级索引

MergeTree的主键定义之后,MergeTree会依据index_grabularity间隔(默认8192行),为数据生成一级索引并保存至primary.idx文件中,索引数据按照Primary Key排序。当用Order By来指定主键的时候索引和数据文件会按照完全相同的排序规则。

2.1稀疏索引

  • 稀疏索引:每一行索引标记对应的是一段数据。Clickhouse使用的就是稀疏索引,由于稀疏索引占用的空间少,所以primary.idx内的索引数据常驻内存,取用速度非常快。
  • 稠密索引:每一行索引标记都会对应到一行具体的数据记录

2.2索引粒度

 

2.3索引数据的生成规则

由于是稀疏索引,所以MergeTree需要间隔index_grabularity间隔行数据才会生成一条索引记录,其索引值会依据声明的主键字段获取。会每间隔8192行取一次主键id值作为索引值,最终会被写入primary.idx文件进行保存。索引如果主键是元组类型的多个列,则隔8192行取所有的主键列的值作为索引值,索引值按照主键字段顺序紧密排列在一起。

2.4索引的查询过程

MarkRange在ClickHouse中是用于定义标记区间的对象。一个具体的数据段即是一个MarkRange。MarkRange与索引编号对应,使用start和end两个属性表示其区间范围。

索引查询过程:

(1)生成查询条件的区间:首先将查询条件转换为条件区间。即便是单个值的查询条件,也会被转换成区间的形式。

Where ID = ‘A003’ => [‘A003’, ‘A003’]
Where ID > ‘A000’ => [‘A000’, +inf]
Where ID < ‘A188’ => [‘-inf’, ‘A188’]
Where ID like ‘A006%’ => [‘A006’, ‘A007’]

(2) 递归交集判断:以递归的形式,依次对MarkRange的数值区间与条件区间做交际判断。从最大的区间[A000, +inf]开始:

  • (2.1)如果不存在交集,则直接通过剪枝算法优化此段MarkRange。

  • (2.2)如果不存在交集,且MarkRange的步长大于8(end-start),,则将此区间进一步拆分成8个子区间(由merge_tree_coarse_index_granularity指定,默认值为8),并重复此规则,继续做递归交集判断。

  • (2.3)如果存在交集,且MarkRange不可再分解(步长小于8),则记录MarkRange则返回。

(3)合并MarkRange区间:将最终匹配的MrakRange聚在一起,合并他们的范围。

3、二级索引

二级索引又称为跳数索引,由数据的聚合信息构建而成。条数索引默认关闭,需要在使用的时候开启。条数索引需要在Create语句内定义,它支持使用元组和表达式的形式声明:

Index index_name expr type index_type(…) GRANULARITY granularity

如果声明了条数索引则会额外生成索引与标记文件(skp_idx_[column].idx与skp_idx_[column].mrk)。

granularity和index_granularity区别是:index_granularity定义了数据的粒度,granularity定义了一行条数索引能够跳过多少个index_granularity区间的数据。

当index_granularity=8192, granularity=3时,数据会按照index_granularity划分为n等分,则二级索引会跨越3个index_granularity区间,取第一个索引列的最小值,以及取第三个区间索引列的最大值,来生成一条索引。

跳数索引的类型:minmax,set,ngrambf_v1和tokenbf_v1。

4、数据存储

4.1各列独立存储

MergeTree中数据按列存储,每列都有一个与之对应的.bin数据文件,数据文件以分区目录形式存放,所以在.bin文件中只会保存当前分区片段内的这一部分数据。

首先数据时压缩的,目前支持LZ4、ZSTD、Multiple和Delta算法等,其次数据会按照ORDER BY的声明排序:最后数据是以压缩数据块的形式写入到.bin文件中的。

4.2压缩数据块

.bin文件是由多个压缩数据块组成,每个压缩数据块的体积,按照其压缩前的字节大小,都被严格控制在64KB~1MB。

其上下限分别由min_compress_block_size(默认65536)与max_compress_block_size(默认1048576)。

MergeTree在数据具体写入过程中,会按照索引粒度(默认情况下每次取8192行)按批次获取数据并进行处理:

  1. 单个批次数据size<64kb:如果单个批次数据小于64kb,则继续获取下一批次数据,直至累积到size>=64kb,生成下一个数据块。
  2. 单个批次64kn<=size<=1MB:如果单个批次数据大小恰好在64kb与1MB之间则直接生成下一个压缩数据块。
  3. 单个批次数据size > 1md:如果单个批次数据直接超过1md,则首先按照1md大小阶段并生成数据块,剩余的执行上述流程。

5、数据标记

5.1 数据标记生成规则

数据标记文件与.bin文件一一对应。每一个列字段[Column].bin文件都有一个与之对应的[Column].mrk数据标记文件,用于记录数据在.bin文件中的偏移量信息。

一行标记数据使用一个元组表示,元组内包含两个整形数值的偏移量信息:(压缩文件中的偏移量,解压缩块中的偏移量)

 

标记数据文件数据示意图

每一行标记数据都表示了一个片段的数据(默认8192行)在.bin压缩文件中的读取位置信息。标记数据不常驻内存,使用LRU缓存策略加快其取用速度。

5.2数据标记工作方式

6、写入和查询的执行过程

6.1写入过程

1、数据写入首先生成分区目录,每一次执行一次写入的INSERT语句都会生成一个新的分区目录(所以每次写的时候别写太少的数据)。在后续的某一个时刻,后台任务会将属于相同分区的目录按照规则合并在一起。

2、按照index_granularity索引粒度,会分别生成primary.idx以及索引(如果声明了二级索引,还会生成二级索引文件)、每一列字段的.mrk数据标记和.bin压缩数据文件

6.2查询过程

如果一条查询语句没有指定任何where条件,或是置顶哦了where条件,但条件没有匹配到任何索引(分区索引、一级索引、二级索引),那么MergeTree就不能预先减小数据范围。后续查询的时候会扫描所有的分区,一级目录内索引段的最大区间,在这种情况下MergeTree依然能借助数据标记,以多线程的方式同时读取多个压缩数据块,以提升性能。

 

6.3数据标记与压缩数据块的对应关系

6.3.1多对一

这种情况是:当一个数据间隔内的数据未压缩大小Size小于64KB,由于会继续读取下一个间隔的数据凑成最小的64KB进行压缩,所以会生成多个数据标记对应一个压缩块中的多个数据间隔。

6.3.2一对一

这种情况是:一个数据间隔内的数据未压缩大小在64KB<SIZE<1MB之内,所以刚好能凑成一个压缩块。

6.3.3一对多

这种情况是:一个间隔内的数据未压缩大小SIZE>1MB

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值