MergeTree 原理

MergeTree 原理

文章目录

  • MergeTree 原理
    • 1. MergeTree的创建方式与存储方式
      • 1.1 创建方式
      • 1.2 存储结构
    • 2. 数据分区
      • 2.1 数据分区规则
      • 2.2 分区命名规则
      • 2.3 一级索引(常驻内存)
        • 2.3.1 索引粒度
        • 2.3.2 生成规则以及查询过程
    • 3. 数据存储(.bin)
    • 4.数据标记
    • 5.总结

Clickhouse最大的特点之一是拥有比较多的表引擎。所有表引擎中最强大的引擎是 合并树结构(*MergeTree)。

1. MergeTree的创建方式与存储方式

如果你现在就有一张clickhouse的表,可以用show create table来查看表的创建。这样看比较直观,如果没有的话,可以按如下的方法进行创建。

1.1 创建方式

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.

1.2 存储结构

目录可以看如下的图:

现在简单说一下图中 的含义:

  • partition:分区目录。
  • checksum.txt:校验文件,使用二进制格式存储。
  • columns.txt:列信息文件,使用铭文存储。
  • count.txt:计数文件。
  • primary.idx:一级索引文件,使用二进制格式存储。用于存放稀疏索引。
  • [Column.bin]:列的数据压缩文件,同时也体现了clickhouse的列存储特性
  • [Column.mrk]:列字段标记文件,将索引与压缩数据文件联系起来
  • [Column.mrk2]:自适应的索引。
  • 最后两个文件是二级索引的相关信息。

下面主要介绍一下数据分区、一级索引、数据存储、数据标记。这边用一张图进行一个简单的概述。

2. 数据分区

需要注意的是MergeTree不能依靠分区的特性,将一张表的数据分布到多个clickhouse的服务节点。

2.1 数据分区规则

分区的规则按照设置的分区ID进行区分。简单来说,clickhouse可以按照用户的设定来对数据的分区。分区的ID生成有四种规则:

  1. 不指定:那么所有的数据都写入到all这个分区。
  2. 整形:直接按照整数形式输出(UINT64)
  3. 日期类型:可以按照YYYYMMDD进行分区名设定。
  4. 其他类型:对于String、FLOAT进行分区的话,会使用128位Hash算法的值作为分区ID的值。

2.2 分区命名规则

上面讲完分区规则,那么疑问来了。

clickhouse是怎么对分区进行命名的呢?

这边可以进入clickhouse的文件目录看一下/var/lib/clickhouse/data

通过文件夹的文件可以看到文件的命名方式如下:

202106-1-2-0

  1. 202106:这个表示第2.1节所定义的分区规则
  2. 1:表示该分区的最小BLOCKNUM
  3. 2:表示该分区最大BLOCKNUM
  4. 0level表示分区的合并次数,这边出现了合并次数的说法。下面将详细说明。

注意:这边的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_0202105_1_4_2

注意:clickhouse出于性能考虑,在生成新的分区(active=1)时保留旧的分区(active=0)

查询时active=0的数据会自动跳过。

2.3 一级索引(常驻内存)

这里由于工作不涉及二级缓存,这边就不做介绍。具体可以看其他博主的博文。

一级索引的主键定义主要由PRIMARY KEY指定,没有指定的话就按照ORDER BY指定。这里主要介绍索引的索引粒度(index_granularity)以及索引的生成规则。

一级索引采用稀疏索引,这就好比分页不会具体到某一行数据。

2.3.1 索引粒度

索引粒度这个参数之前提及过,其默认值为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的数据。因为光靠一级索引无法完成数据的查询操作。

讲到这个地方可能开始晕了,这个时候可以看看一开始的图缓一缓。

2.3.2 生成规则以及查询过程

在讲索引的查询过程前,先讲一下索引的生成规则。

假设我们有一张成绩的表,其中的数据按照grade/grade+date进行分区。

下图展示了按照这两种方式生成索引的构成。

下面介绍一下查询的过程。具体思想可以描述为交集+剪枝的过程:

  • Step 1:生成查询的区间,首先将查询条件转化为区间形式

    WHERE grade=60 ----> [60,60]  #突然想到:需要注意的是clickhouse是大小写敏感的,所以要特别注意大小写的区分
    WHERE grade >40 -----> (40,+inf)
    
  • Step 2递归求交集判断(这里参照分治递归的思想)

    1. 如果不存在交集:直接剪枝
    2. 如果存在,且markrange可再分(end-start>8)则对查询的MarkRange数值区间进行拆分,重复此过程
    3. 如果存在,且markrange不可再分(end-start<8),则记录markrange进行返回
  • Step 3:合并MarkRange区间

这里的8不是固定值,可以通过设置merge_tree_coarse_index_granularity进行修改。

3. 数据存储(.bin)

clickhouse使用列存储,各列独立存储。

在存储数据的方式上面,clickhouse不是一股脑把数据存入bin文件,而是通过压缩的方式进行存储。当前的压缩支持LZ4、ZSTD、MULTIPLE和DELTA几种,默认使用LZ4算法。所有数据会事先按照ORDER BY进行排序。

bin中存在多个压缩块。

一个压缩数据块由headerbody组成,如下图所示:

其中,压缩方法

  1. LZ4:0x82
  2. ZSTD:0x90
  3. Multiple:0x91
  4. Delta:0x92

下面讲一下MergeTree的数据写入过程。

如果一批数据(index_granularity)进行写入遵循一下的规则

  1. 单个批次数据大小<64KB:获取下一批次的数据,直至累计到size>=64KB ,生成下个数据块。
  2. 单个批次64KB<=数据大小<=1MB:直接生成下一个数据块。
  3. 单个批次数据大小>1MB:更具1MB的大小对数据进行截断,剩余的数据按照上述的过程进行操作。

4.数据标记

数据标记主要将索引与数据压缩块的数据进行关联。这些信息被记载在.mrk文件中。对应方式如下图:

跟一级索引不同的是:mrk不能常驻内存,使用LRU的方式加快取用速度。

需要注意的是,压缩文件中的偏移位置的大小=压缩块大小+头部的字节数(8字节)

数据标记的工作方式:MergeTree在读取数据时,必须通过标记数据的位置信息才可以找到所需要的数据。整个过程分为读取压缩数据块和读取数据两个过程。

  1. 读取压缩块:在读取的时候,不必加载所有的bin压缩块,而是根据标记文件来定位压缩文件中的偏移量。
  2. 读取数据:在解压读取数据时不会对文件进行一次扫描,而是以index_granularity来加载特定的一小段。这也是需要mrk中压缩块中偏移的数据。

5.总结

分区、索引、标记、压缩好比一件艺术品。分解成一块块看似平平无奇,整合起来就可以惊现他人。具体的过程可以根据《Clickhouse 原理解析与应用实践》来学习。

你可能感兴趣的:(clickhouse,数据库,olap,大数据)