Clickhouse MergeTree 详细解析

在Clickhouse众多的表引擎中,MergeTree表引擎及其家族最为强大,在生产环境中的绝大数场景,都会使用此系列的表引擎。

只有MergeTree系列的表引擎才支持主键索引,数据分区,数据副本,数据采样这些特性,只有此系列的表引擎才支持ALTER操作。

MergeTree表引擎在写入一批数据的时候,数据总会以数据片段的形式写入磁盘,并且数据片段不可修改。为了避免片段过多,clickhouse会通过后台的线程,定期合并这些数据片段,属于不同分区的数据片段会被合并成一个新的片段,这种数据片段往复合并的特点正式合并树(MergeTree)名称的由来。

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
[PARTITION BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[TTL expr [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'], ...]
[SETTINGS name=value, ...]

解释:
必填项:
1.ENGINE:创建MergeTree的表引擎指定ENGINE = MergeTree()
2.ORDER BY语句:sorting key 排序键,用于指定在一个数据片段内数据以何种标准排序。默认情况下是主键primary key
与排序键相同。排序键可以单个列字段,也可以多个列字段。
选填项:
1.PARTITION BY :分区键 用于指定表数据以何种标准进行分区。分区键可以单个列字段,也可以是通过元祖形式使用的多个列字段,还可以支持使用列表达式。
若不声明分区键则clickhouse会生成一个名为all的分区。合理使用分区 可以有效减少查询数据文件的扫描范围。
2.primary key:主键 声明后会按照主键字段生成一级索引,用于加速表查询。默认情况下主键与排序键相同,
所以通常
直接使用order by 指定主键,无须刻意通过primary key声明。在一般情况下,在单个数据片段内 数据与一级索引
以相同的规则升序排列。
MergeTree主键允许存在重复数据(ReplacingMergeTree可以去重)。

3.sample by:抽样表达式,用于声明数据以何种标准进行采样。若使用了此配置选项则在主键的配置中也需要声明同样的表达式。
抽样表达式需要配合sample by 子查询使用,这项功能对于选取抽样数据十分有用。
4.TTL:指定表级别的数据存活策略
5.settting部分:
   index_granularity,:默认值8192,表示索引的粒度,即MergeTree的索引在默认情况下,每间隔8192行数据才生成一条索引。通常不需要修改此参数。
   index_granularity_bytes :默认值10Mb(10*1024*1024)表示自适应间隔大小的特性,即根据每一批写入数据的体量大小,动态划分间隔大小。设置为0表示不启用自适应功能。
自19.11版本新增。
   enable_mixed_granularity_parts :设置是否开启自适应索引间隔的功能,默认开启。
   use_minimalistic_part_header_in_zookeeper :
   min_merge_bytes_to_use_direct_io :默认值10 * 1024 * 1024 * 1024 =10M
   merge_with_ttl_timeout :默认值1天=86400s 
   write_final_mark :
   merge_max_block_size :
   storage_policy :指定存储策略
   min_bytes_for_wide_part和min_rows_for_wide_part :

  

2.MergeTree的存储结构:

MergeTree表引擎中的数据是拥有物理存储的,数据会按照分区目录的形式存储到磁盘之上。

创建一个MergeTree分区表:


查看物理结构:

table_name
    |---partition 1
    |    |--checksums.txt
    |    |--columns.txt
    |    |--count.txt
    |    |--primary.idx
    |    |--[column].bin
    |    |--[column].mrk
    |    |--[column].mrk2
          (以上为基础文件)
    |    |--partition.dat
    |    |--minmax_[column].idx
          (以上为使用了分区键的时候生成的文件)
    |    |--skp_idx_[column].idx
    |    |--skp_idx_[column].mrk
          (以上为使用二级索引时候才会生成的文件)
    |---partition_2
    |
    |---partition_n



解释:
1.partition 分区目录
partition_n目录下的各类数据文件都是以分区形式被组织存放的,属于相同分区的数据最终会被合并到同一个分区目录内。
2.checksums.txt:校验文件,使用二进制存储,保存了各类文件的size大小和size的哈希值,用于快速校验文件的完整性和正确性。
3.columns.txt:列信息文件,使用文本文件存储,用于保存分区下的列字段信息。
4.count.txt:计数文件,文本文件存储,用于记录当前数据分区目录下数据的总行数。
5.primary.idx:以及索引文件,使用二进制格式存储。用于存放稀疏索引,一张MergeTree表只能声明一次一级索引
(通过order by或者primary key)。借助稀疏索引在数据查询的时候能够排除主键范围之外的数据文件,从而减少数据扫描范围,加速查询速度。
6.[column].bin:数据文件,使用压缩格式存储,默认使用LZ4压缩格式,用于存储某一列的数据。由于MergeTree采用列式存储,每个列字段都拥有独立的bin数据文件,并以列字段命名。
7.[column].mrk列字段标记,使用二进制格式存储。标记文件中保存了bin文件中数据的偏移量信息,标记文件与稀疏文件对齐,又与bin文件一一对应,所以MergeTree通过标记文件建立了primary.idx稀疏索引与bin数据文件的隐射关系。
首先通过primary.idx找到对应数据的偏移量信息(.mrk),再通过偏移量直接从bin文件中读取数据。由于.mrk标记文件与.bin文件一一对应,所以MergeTree中的每个列字段都会拥有与其对应的.mrk文件。

8.[column].mrk2 如使用了自适应大小的索引间隔,则标记文件会以.mrk2命名。工作原理和作用和.mrk标记文件相同。
9.partition.dat和minmax_[column].idx:
若使用了分区键则会额外生成partition.dat和minmax索引文件,均使用二进制格式存储。partition.dat用于保存当前分区下
分区表达式最终生成值,minmax索引文件用于记录当前分区字段对应原始数据的最小值和最大值。
在分区索引作用下,进行数据查询时候能够快速跳过不必要的数据分区目录,从而减少最终需要扫描的数据范围。
10.skp_idx_[column].idx和skp_idx_[column].mrk:
若在建表语句中声明了二级索引则会额外生成相应的二级索引与标记文件,他们同样用二进制存储。
二级索引在clickhouse中又称之为跳数索引,目前拥有minmax,set,ngrambf_v1和tokenbf_v1四种类型。
这些索引的目标和一级稀疏索引相同,为了进一步减少所需要扫描的数据范围,以加速整个查询过程。

3.数据分区

在clickhouse数据中,数据分区是针对本地数据而言,是对数据的一种纵向切分。MergeTree并不能依靠分区的特性,将一张表的数据分布到多个clickhouse服务节点。横向切分是数据分片的能力(shard).

1.数据的分区规则:
MergeTree数据分区规则有ID决定,而具体到每个数据分区所对应的ID则是由分区键的取值决定的,分区键支持使用任何一个或者
一组字段表达式声明,其业务语义可以使年月日或者组织单位等任何一种规则,针对取值数据类型的不同,分区ID的生成逻辑目前
有四种规则:

1.不指定分区键:不使用partition by 声明任何分区表达式 则分区ID默认取名为all,所有数据写入all分区。
2.使用整型:若分区键取值属于整型(兼容Uint64包含有符号整型和无符合整型)且无法转换为日期类型YYYYMMDD格式则
直接按照整型的字符形式输出,作为分区ID的取值。
3.使用日期类型:若分区键取值属于日期类型,或者可以转为YYYYMMDD格式的整型则按照使用YYYYMMDD进行格式化后的字符形式
输出,并作为分区ID的取值。
4.使用其他类型:若分区键取值不属于整型或者日期类型,如String,float则通过128位的Hash算法取其Hash值作为分区ID的取值。
数据在写入时,会对照分区ID落入相应的数据分区。


2.分区目录的命名规则:
对于MergeTree最核心的特点是其分区目录的合并动作,而分区目录的命名中可以解读出合并逻辑。
一个完整分区目录的命名公式如下:
PartitionID_MinBlockNum_MaxBlockNum_Level
PartitionID:分区ID
MinBlockNum和MaxBlockNum:最小数据库编号和最大数据库编号,这里的BlockNum是一个整型的自增长编号。
一个MergeTree表
在内部全局累加,从1开始每当新创建一个分区目录,计数器就嫁1.对一个新分区MinBlockNum和MaxBlockNum一样,当分区目录
发生合并的时候,新产生的合并目录inBlockNum和MaxBlockNum有另外的取值规则。
Level:合并的层级,也可以理解为某个分区被合并过的次数,数值越大则合并的次数越多。
对于一个新创建的
分区目录初始值是0 ,此后以分区为单位若相同分区发生合并动作则在相应分区内计数器加1.


3.分区目录的合并过程:
1.MergeTree的分区目录是在数据写入过程中被创建的
2.MergeTree的分区目录伴随着每一批数据的写入(一次insert语句),mergetree都会生成一批新的分区目录,即便不同批次写入
的数据属于相同分区,也会生成不同的分区目录。

对于同一分区也会存在多个分区目录的情况,在此之后的时刻,(写入10--15分钟,也可以手动执行optimize查询语句),clickhouse会
通过后台任务再将属于相同分区的多个目录合并成一个新的目录。
已经存在的旧目录并不会立即被删除,而是在之后的某个时刻通过后台被删除(默认8分钟)。

同属于一个分区的多个目录,在合并之后会形成一个全新的目录,目录中的索引和数据文件也会相应的进行合并。新目录名称的合并规则如下:
MinBlockNum:取同一分区内所有目录中最小的MinBlockNum值
MaxBlockNum:取同一分区内所有目录中最大的MaxBlockNum值
Level:取同一分区内最大Level值并加1

4.一级索引

MergeTree的主键使用primary key定义,待主键定义之后,MergeTree会依据index_granularity间隔(默认8192行),为数据表生成一级索引并保存至primary.idx文件内,索引数据按照primary key排序,相比使用primary key定义,更为常见的是通过order by指代主键。在此情况下,primary key和order by 定义相同,索引文件primary.idx和数据.bin会按照完全相同的规则排序。

注意:order by 和primary key定义有差异的应用场景是SummingMergeTree引擎。

稀疏索引:

primary.idx文件内的一级索引采用稀疏索引实现。在稀疏索引中每一行索引标记对应的是一段数据,而非一行。而稠密索引中一行索引标记都会被对应到一行具体的数据记录。

稀疏索引仅用少量的索引标记就能够记录大量数据的区间位置信息,且数据量越大优势越明显。默认的索引粒度8192,MergeTree只需要12208行索引标记就能为1亿行数据记录提供索引。稀疏索引占用空间小,所以primary.idx内的索引数据常驻内存,且速度极快。

索引粒度:

数据以index_granularity(8192)被标记为多个小的区间,其中每个区间做多8192行数据。MergeTree使用MarkRange表示一个具体的区间,并通过start和end表示其具体的范围。index_granularity不但作用于一级索引还会影响标记文件和数据文件。因为仅有一级索引文件是无法完成查询工作的,需要借助于标记来定位数据,所以一级索引和和数据标记的间隔粒度相同,彼此对齐,而数据文件也会按照index_granularity的间隔粒度生成压缩数据块。

索引数据的生成规则:

MergeTree需要间隔index_granularity行数据才会生成一条索引记录,其索引值会依据声明的主键字段获取。

索引的查询过程:

索引查询其实就是两个数值区间的交集判断,其中一个区间是由基于主键的查询条件转换而来的条件区间;而另一区间则是与MarkRange对应的数值区间。

5.二级索引

MergeTree支持二级索引,有称之为跳数索引,是由数据的聚合信息构建而成。根据索引类型的不同,其聚合信息的内容也不同,跳数索引的目录也是帮助查询,减少数据扫描的范围。

Clickhouse> select name,value,description from system.settings where name like '%skipping_indices%';

SELECT 
    name,
    value,
    description
FROM system.settings
WHERE name LIKE '%skipping_indices%'

┌─name─────────────────────────────────────┬─value─┬─description──────────────────────────────────────────────────────┐
│ allow_experimental_data_skipping_indices │ 1     │ Obsolete setting, does nothing. Will be removed after 2020-05-31 │
└──────────────────────────────────────────┴───────┴──────────────────────────────────────────────────────────────────┘

1 rows in set. Elapsed: 0.005 sec. 

SELECT version()

┌─version()───┐
│ 20.6.1.4066 │
└─────────────┘
此参数默认是开启的,即允许跳数索引。

二级索引需要在create语句内定义,支持用元组和表达式的形式声明。和一级索引一样,若在创建表语句中声明了二级索引,则会生成相应的索引与标记文件(skpi_idx_[Column].idx与skip_idx_[Column].mrk).

granularity与index_granularity的关系:

不同的二级索引之间,除了他们自身独有的参数之外,还都共同拥有granularity参数。对二级索引 index_granularity定义了数据的粒度,而参数granularity定义了聚合信息汇总的粒度。即granularity定义了一行跳数索引能够跳过多少个index_granularity区间的数据。

 

跳数索引的类型

目前MergeTree共支持4中跳数索引,分别是minmax,set,ngrambf_v1和tokenbf_v1.一张表支持同时声明多个跳数索引。

Clickhouse> create table t_skip(id varchar(8),website String,code varchar(32),createtime datetime,index ix_id id type minmax granularity 5,index ix_length(length(id)*8) type set(2) granularity 5,index ix_id_code (id,code) type ngrambf_v1(3,256,2,0) granularity 5,index ix_token id type tokenbf_v1(256,2,0) granularity 5)engine=MergeTree() partition by toYYYYMM(createtime) order by id primary key id;

CREATE TABLE t_skip
(
    `id` varchar(8),
    `website` String,
    `code` varchar(32),
    `createtime` datetime,
    INDEX ix_id id TYPE minmax GRANULARITY 5,
    INDEX ix_length length(id) * 8 TYPE set(2) GRANULARITY 5,
    INDEX ix_id_code (id, code) TYPE ngrambf_v1(3, 256, 2, 0) GRANULARITY 5,
    INDEX ix_token id TYPE tokenbf_v1(256, 2, 0) GRANULARITY 5
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(createtime)
PRIMARY KEY id
ORDER BY id

Ok.

0 rows in set. Elapsed: 0.004 sec. 

说明:

1.minmax:minmax索引记录了一段数据内的最小值和最大值,其索引的作用类似于分区目录的minmax索引,能够快速跳过无用的数据区间。GRANULARITY 5表示极值计算涉及每5个index_granularity区间的数据。

2.set:set索引直接记录了声明字段或表达式的取值(唯一值,无重复),其完整形式为set(max_rows),其中的max_rows 是一个阈值,表示在一个index_granularity内,索引做多记录的数据行数。若max_rows=0 则表示无限制。

ix_length length(id) * 8 TYPE set(2) GRANULARITY 5 表示set索引会就索引id的长度*8后的取值,每个index_granularity内最多记录100条。

3.ngrambf_v1
ngrambf_v1索引记录的是数据短语的布隆表过滤器,只支持String和FixedString数据类型。ngrambf_v1只能够提升in、notIn、like、equals和notEquals查询的性能,其完整形式为ngrambf_v1(n,size_of_bloom_filter_in_bytes,number_of_hash_functions,random_seed).
这些参数是一个布隆过滤器的标准输入,如果你接触过布隆过滤器,应该会对此十分熟悉。它们具体的含义依次如下:

n:token长度,依据n的长度将数据切割为token短语;
size_of_bloom_filter_in_bytes:布隆过滤器的大小;
number_of_hash_functions:布隆过滤器中,使用Hash函数的个数;
random_seed:Hash函数的随机种子。
 ngrambf_v1索引会依照3的粒度将数据切割成短语token,token会经过2个Hash函数映射后写入,布隆过滤器大小为256字节
 
示例说明:

INDEX ix_id_code (id, code) TYPE ngrambf_v1(3, 256, 2, 0) GRANULARITY 5
ngrambf_v1索引会按照3的粒度将数据切割成短语token,token会经过2个Hash函数隐射后再被写入,布隆过滤器大小为256字节。

 4.tokenbf_v1:

 tokenbf_v1索引是ngrambf_v1的变种,同样也是一种布隆过滤器索引。tokenbf_v1除了短语token的处理方法外,其他
  其他与ngrambf_v1是完全一样的。tokenbf_v1会自动按照非字符、数字的字符串分割token.

示例:INDEX ix_token id TYPE tokenbf_v1(256, 2, 0) GRANULARITY 5

6.数据存储

6.1 列独立存储:

在MergeTree中,数据按照列存储,具体到每个字段数据也是独立存储的,每个列字段都拥有一个与之对应的.bin文件,这些bin文件承载着数据的物理存储。数据文件以分区目录的形式被组织存储,在bin文件中只会保存当前分区片段内的这部分数据。按照列存设计优势如下:

1.可以更好的进行数据压缩,相同类型的数据放在一起,对压缩更加友好

2.能够最小化数据扫描的范围。

MergeTree的存储上:

1数据是经过压缩的,目前支持LZ4,ZSTD,Multiple和Delta几种算法,默认使用LZ4算法

2.数据会事先按照ORDER BY的声明排序

3.数据是以压缩数据块的形式被组织写入bin文件中

6.2 压缩数据块:

数据压缩块由头信息和压缩数据两部分组成。头信息固定使用9位字节表示,具体由一个UInt8整型和2个UInt32 整型组成,分别代表算法类型,压缩后的数据大小和压缩前的数据带下。

CompressionMethod_CompressedSize_UncompressedSize

压缩算法:LZ4 0x82 ZSTD 0x90 Multiple 0x91 Delta 0x92

Clickhouse提供了工具能够查询某个bin文件中压缩数据的统计信息:

# clickhouse-compressor --stat < /var/lib/clickhouse/data/datasets/visits_v1/20140317_20140323_7_12_1/JavaEnable.bin 
65536   27136
65536   31981
65536   29593
65536   25322
65536   28065
65536   24682
65536   23316
65536   32860
65536   30594
65536   31556
65536   30562
65536   30268
65536   29249
65536   31384
65536   28867
43667   21520

其中每一行数据代表着一个压缩数据块的头信息,分别表示该压缩块中为压缩数据的大小和压缩后数据大小
(输出的信息与物理存储的顺序刚好相反)

每个压缩数据块的体积,按照其压缩前的数据字节大小,都被严格控制在64KB--1MB,其上限和下限分别由参数min_compress_block_size(默认65536)与max_compress_block_size(默认1048576)指定。一个压缩数据块最终的大小,则和一个间隔(index_granularity)内数据的实际大小相关。

MergeTree在数据具体的写入过程,会依照索引粒度,按照批次获取数据并进行处理。若把一批数据的未压缩大小设置为size则整个写入过程则遵循以下规则:1.单个批次数据size <64KB 则继续获取下一批数据,直至累计到size >=64KB 时候生成一个数据压缩快 2.单个批次数据64KB ---1MB之间则直接生成一个压缩数据块 3.单个批次数据size >1MB 首先按照1MB大小阶段并生下一个压缩数据块。剩余的数据按照上述规则执行,此时会出现一个批次数据生成多个压缩数据块的情况。

结论:一个bin文件是由一个多个压缩数据库组成,每个压缩快大小在64KB--1MB之间,多个数据块之间则按照写入顺序收尾相接,紧密地排列在一起。

在bin文件中引入压缩数据库的目的至少有两个:1.压缩后能够有效减少数据大小,降低存储空间并加速数据传输效率,但是数据的压缩和解压缩本身也会带来额外的性能损耗。所以需要控制压缩数据快的大小,以求在性能损耗和压缩率之间寻求平衡。2.在具体读取某一列数据的时候,首先需要将数据加载到内存并解压 这样才能进行后续的数据处理,通过压缩数据块,可以在不读取整个bin文件的情况下将读取粒度降低到压缩数据块级别,从而进一步缩小数据读取的范围。

7.数据标记

数据标记的特征 即数据标记和索引区间的是对齐的,均按照index_granularity的粒度间隔。为了能够与数据衔接,数据标记文件与bin文件一一对应。即每个列字段[Column].bin文件都有一个与之对应的[Column].mrk数据标记文件,用于记录数据在bin文件的偏移量信息。

标记数据和一级索引数据不同,它并不常驻内存,而是使用LRU存储策略加快其读取速度。

8.分区索引和标记 数据压缩的协同: 

写入过程:

数据写入的第一步是生产分区目录,伴随着每一批数据的写入,都会生成一个新的分区目录。在后续的某一时刻,属于相同分区的目录会按照规则合并到一起;接着按照index_granularity索引粒度,会分别生成primary.idx一级索引(若声明了二级索引则会穿创建二级索引文件),每个列字段的.mrk数据标记和.bin压缩数据文件。 生成的文件其中索引和标记区间是对齐的,而标记与压缩块则根据区间数据的大小会生成多对一,一对一和一对多三种关系。

查询过程:

数据查询的本质,可以看做是一个不断减小数据范围的过程。在最理想的情况下,MergeTree首先一次借助于分区索引,一级索引和二级索引将数据扫描范围缩至最小。再借助于数据标记,将需要解压与计算的数据范围缩至最小。

minmax.idx(分区索引)---primary.idx(一级索引)---skip_idx.idx(二级索引)---.mrk (标记文件)----.bin(数据压缩文件)。

若一条查询语句没有指定任何的where条件,或指定了where条件但条件么有匹配到任何索引(分区索引,一级索引和二级索引),那么MergeTree就不能预先减小数据范围。在后续继续数据查询的时候,则会扫描所有分区目录,以及目录内索引段的最大区间。n虽然不能减少数据范围,但是MergeTree仍然能够借助数据标记,以及多线程的形式同时读取多个压缩数据块,以提升性能。

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

由于压缩数据库的划分,与一个间隔index_granularity 内的数据大小相关,每个要数据块的体积都被严格控制在64KB---1MB之间,而一个间隔的数据index_granularity的数据,又会只产生一行数据标记。则根据一个间隔内数据的实际字节大小,数据标记和压缩数据块之间会产生三种不同的对应关系。 

多对一

多个数据标记对应一个数据压缩块。当一个index_granularity内的数据为压缩大小size小于64KB则会出现此种对应关系。

一对一

当一个数据标记对应一个压缩数据块,当一个index_granularity内的数据未压缩大小size等于64KB且小于等于1MB的时候会出现这种对应关系。

一对多

一个数据标记对应多个压缩数据块,当一个index_granularity的数据未压缩大小size直接大于1MB的时候则会出现此种关系。

参考:

https://clickhouse.tech/docs/en/engines/table-engines/mergetree-family/mergetree/

https://blog.csdn.net/weixin_39992480/article/details/105019953

https://www.jianshu.com/p/20639fdfdc99

  • 6
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值