导语:ClickHouse是面向OLAP Query场景设计的,由俄罗斯的Yandex于2016年开源的DBMS。它之所以能够获得极致的查询性能和底层极致的存储设计密切相关,本文以实战的方式对MergeTree存储引擎的存储原理进行解析,与各位同仁共享。
一、从创建⼀张表开始
⾸先通过创建⼀张表直观的感受⼀下:
编辑
● 我们观察⼀下存储目录变化情况
编辑
存储目录下生成了⼀个和表名称⼀样的软件接口,指向了⼀个以UUID命名的文件。
● 我们插入⼀些数据,观察目录下的内容
编辑
● 目录内容
编辑
● 我们对目录和文件关键内容进行⼀些说明:
all_1_1_0: 称为数据片段分区名称_MinBlockNum_MaxBlockNum_Level,如果表没有分区则分区名称为all,如果有分区则分区名称为具体分区值。后台进程会把多个数据片段按照LSM规则进行合并 。
[column].bin:以⼆进制形式对表中数据进行存储。由于建表语句中声明了min_rows_for_wide_part和min_bytes_for_wide_part都为0,CK以每⼀列存储为⼀个数据文件进行存储。
[column].mrk2:column信息的标记文件,记录列字段的元数据信息,后面会对此文件存储内容进行讲解。
primary.idx:索引文件,用于存放稀疏索引。
二、索引文件
ClickHoue的主键索引是⼀种在索引构建成本和索引效率上相对平衡的稀疏索引。
primary.idx是表的主键索引,ClickHouse对主键索引的定义和传统数据库的定义稍有不同,CK主键不能去重,但仍然有快速查找主键行的能力。ClickHouse的主键索引存储的是每⼀个颗粒中起始行的主键值,而MergeTree存储中的数据是按照主键严格排序的。
所以当查询给定主键条件时,我们可以根据主键索引确定数据可能存在的颗粒区间,再结合下面介绍的Mark标识,我们可以进⼀步确定数据在列存文件中的位置区间。通过解析索引文件内容可以理解primary.idx存储内容。
编辑
RI_BDTL.test表的索引字段为ID,字段类型为UInt64,所以⼀个ID值占用8个字节进行存储。第⼀个8字节:01 00 00 00 00 00 00 00 表示数值1, 第⼆个8字节为 04 00 00 00 00 00 00 00 表示数值4。最后我们发现索引文件中共存储了4个数值,分别为1、4、7、9。
这里引入建表语句中的index_granularity=3表示索引间隔为3。即插入的数据中间隔3行数据获取⼀个索引值。
● 索引文件总结
索引文件内容按照index_granularity参数设置的间隔紧凑存储,primary.idx文件中共存储了4个索引值分别是1、4、7、9。
三、[COLUMN].bin文件
[COLUMN].bin 文件是按照BLOCK组织而成。⼆进制文件查看⽐较复杂不在此文表述。
● BLOCK格式如下:
编辑
1. Checksum,16 Bytes,用于对后面的数据进行校验。
2. Compression algorithm,1 Byte,默认是LZ4,编号为0x82。
3. Compressed size,4 Bytes,其值等于Compression algorithm + Compressed size + Decompressed size + Compressed data的⻓度。
4. Decompressed size,4 Bytes,数据解压缩后的⻓度。
5. Compressed data,压缩数据,⻓度为Compressed size - 9。压缩数据由"颗粒"组成。颗粒是CK进行数据查询和存储的最小不可分割的数据集。
四、mrk2文件
[column].mrk2存储列字段的元数据信息,包括压缩数据块偏移信息和解压数据块的偏移信息和压缩数据块的行数。mrk2文件的作用在于对齐了索引文件和数据颗粒,在用户查询数据过程起到承前启后的作用。
理解mrk2文件作用需要结合bin文件⼀起,我们知道bin文件是由block组成,而block中存储数据信息的是颗粒。颗粒是数据按行划分时用到的逻辑概念。
颗粒的大小通过表引擎参数index_granularity和index_granularity_bytes控制。颗粒的行数在[1, index_granularity]范围内,这取决于行的大小。如果单行的⼤小超过了index_granularity_bytes设置的值,那么⼀个颗粒的⼤小会超过index_granularity_bytes。这种情况颗粒的⼤小等于该行的⼤小。
mrk2文件的⼀个标记对应⼀个bin文件中的⼀个颗粒。
mrk2文件中⼀个标记又对应primary.idx的⼀个索引。
同样,接下来,我们观察⼀下mark文件中的内容。
编辑
● mrk2文件中最小单元mark块格式如下:
编辑
1. Offset in compressed fifile,占用8字节,代表该标记指向的压缩数据块在[column].bin文件中的偏移量 。
2. Offset in decompressed block,占用8字节,代表该标记指向的解压缩数据块在[column].bin文件中的偏移量。
3. Rows count,占用8字节,代表标记快指向的行数。
我们从ID.mrk2文件中按照24个字节⼀个间隔观察数据存储情况,⼀共存储了4个24字节的内容。即4个mark块。
编辑
● 总结 > 索引文件primary.idx中的索引值对应下标和mrk2文件中的mark块下标对⻬。
五、索引文件、mrk2文件、bin文件
存储对应关系
编辑
六、总结
本文中通过创建⼀张MergeTree表入手,分析了底层文件存储格式。包括索引文件、bin文件、mrk2文件以及它们之间的对应关系,⼀张表由按主键排序的数据片段组成。当数据插入到表中时,会创建多个数据片段并按主键排序。
例如RI_BDTL.test表主键是ID,则数据片段会按照ID进行排序后插入到数据片段中。不同批次数据会被分成不同的片段,CK后台进程合并数据片段以便⾼效存储。不同分区的数据片段不会进行合并。
由于数据插入的顺序不同,合并机制并不能保证具有相同主键的行全部合并到同⼀个数据片段中。bin文件由若干个BLOCK组成,BLOCK中压缩数据最小数据集叫颗粒。每个数据片段会在逻辑上分割成颗粒。CK不会对行或值进行拆分, 所以每个颗粒都会包含整数行。索引文件通过标记文件能直接定位到颗粒的位置。