文章目录
一、MergeTree的存储结构
Merge表引擎中的数据是拥有物理存储的,数据会按照分区目录的形式保存的磁盘上,其完整的存储结构如图6-2所示。

图6-2 MergeTree在磁盘上的物理存储结构
从图6-2可以看出,一张数据表的完整物理结构分为3个层级,依次是数据表目录、分区目录及各分区下具体的数据文件。我们接下来依次介绍它们的作用。
-
partition:分区目录,略
-
checksums.txt:校验文件,使用二进制格式存储。它保存了余下各类文件(priamry.idx、count.txt)的size大小及size的哈希值,用于快速校验文件的完整性和正确性。
-
columns.txt:列信息文件,使用明文格式存储。用于保存此数据分区下的列字段信息(不同的分区下可以不一样吗),例如:

-
count.txt:计数文件,使用明文格式存储。用于记录当前数据分区目录下数据的总行数,例如:

-
primary.idx:一级索引文件,使用二进制格式存储。用于存放稀疏索引,一张MergeTree表只能声明一次一级索引(通过Order By 或者 Primary Key)。借助稀疏索引,在数据查询时能够排除主键条件范围之外的数据文件,从而有效减少数据扫描范围,加速查询速度。
-
[Column].bin:数据文件,使用压缩格式存储,默认为LZ4压缩格式,用于存储某一列的数据。由于MergeTree采用列式存储,所以每一个列字段都拥有独立的.bin数据文件,并以列字段名称命名(例如CounterId.bin、EventData.bin等)。
-
[Column].mrk:列字段标记文件,使用二进制格式存储。标记文件中保存了.bin文件中的数据偏移量信息。标记文件与稀疏索引对齐,又与.bin文件一一对应,所以MergeTree通过标记文件建立了primary.idx稀疏索引与.bin数据文件之间的映射关系。即首先通过稀疏索引(primary.idx)找到对应数据的偏移量信息(.mrk),再通过偏移量直接从.bin文件中读取数据。由于.mrk标记文件与.bin文件一一对应,所以MergeTree中的每个列字段都会拥有与其对应的.mrk文件(例如CounterId.mrk、EventDate.mrk等)。
-
[Column].mrk2:如果使用了自适应大小的索引间隔,则标记文件会以.mrk2命名。它的工作原理和作用与.mrk标记文件相同。(为什么不能把.mrk和.bin合并了呢)
-
partition.dat与minmax_[Column].idx:如果使用了分区键,例如PARTITION BY EventTime,则会额外生成partition.dat与minmax索引文件,它们均使用二进制格式存储。partition.dat用于保存当前分区下分区表达式最终生成的值;而minmax索引用于记录当前分区下分区字段对应原始数据的最小和最大值。(为什么是原始数据呢,我们如果按月分区了,还可以按天过滤吗)。例如EventTime字段对应的原始数据为2019-05-01、2019-05-05,分区表达式为PARTITION BY toYYYYMM(EventTime)。partition.dat中保存的值将会是2019-05,而minmax索引中保存的值将会是2019-05-01 2019-05-05。
-
skp_idx_[Column].idx与skp_idx__[Column].mrk:如果在建表语句中声明了二级索引,则会额外生成相应的二级索引与标记文件,它们同样也使用二进制存储。二级索引在ClickHouse中又称跳数索引,目前拥有minmax、set、ngrambf_v1和tokenbf_v1四种类型。这些索引的最终目标与一级稀疏索引相同,都是为了进一步减少所需扫描的数据范围,以加速整个查询过程。
二、分区
MergeTree的分区目录不是在建立之后一成不变的,在其他数据库中,追加数据后后路自身不会发生变化,只是在相同分区目录中追加新的数据文件。而MergeTree完全不同,伴随着每一批数据写入(一次INSERT语句),MergeTree都会生成一批新的分区目录。即便不同批次写入的数据属于相同分区,也会生成不同的分区目录。也就是说,对于同一个分区而言,也会存在多个分区目录的情况,在之后的某个时刻(写入后的10-15分钟,也可以手动执行opitmize查询语句),ClickHouse会通过后台任务将属于相同分区的多个目录合并成一个新的目录。已经存在的旧分区目录并不会被立即删除,而是在之后的某个时刻通过后台任务删除。
属于同一个分区的多个目录,在合并之后会生成一个全新的目录,目录中的索引和数据文件也会相应地进行合并。(感觉合并的成本比较高)
三、一级索引
3.1 稀疏索引
primary.idx文件内的一级索引采取稀疏索引实现。稀疏索引对比稠密索引的优势显而易见,它仅需使用少量的索引标记就能记录大量数据的区间位置信息,且数据量越大优势越位明显。以默认的索引粒度(8192)为例,MergeTree只需12208行索引就能为1亿行数据记录提供索引。由于稀疏索引占用空间极小,所以primary.idx内的索引数据常驻内存,取用速度自然极快。

3.2 索引粒度
索引粒度就如同标尺一般,会丈量整个数据的长度,并依照刻度对数据进行标注,最终将数据标记成多个间隔的小段,如下图所示。

数据以index_granularity的粒度(默认8192)被标记成多个小的区间,其中每个区间最多8192行数据。MergeTree使用MarkRange表示一个具体的区间,并通过start和end表示其具体的范围。index_graularity的命名虽然去了索引而字,但它不单只作用于一级索引(.idx),同时也会影响数据标记(.mrk)和数据文件(.bin)。因为仅有一级索引自身是无法完成查询工作的,它需要借助数据标记才能定位数据,所以一级索引和数据标记的间隔粒度相同(同为index_granularity行),彼此对齐。而数据文件也会按照index_granularity的间隔粒度生成压缩数据块。
3.3 索引的查询过程
首先,我们需要先了解什么事MarkRange。MarkRange在ClickHouse中是用于定义标记区间的对象。通过先前的介绍已知,MergeTree按照index_granularity的间隔粒度,将一段完整的数据划分成了多个小的间隔数据段,一个具体的数据段即是一个MarkRange。MarkRange与索引编号对应,使用start和end两个属性表示其区间范围。通过与start及end对应的索引编号的取值,即能够得到它所对应的数值区间。而数值区间表示了此MarkRange包含的数据范围。
示例:假设现在有一份测试数据,共192行记录。其中,主键ID为String类型,ID的取值从A000开始,后面依次为A001、A002…直至A192为止。MergeTree的索引粒度index_granularity=3,根据索引的生成规则,primary.idx文件内的索引数据会如图6-10所示。

根据索引数据,MergeTree会将此数据片段划分成192/3=64个小的MarkRange,两个相邻的MarkRange相距的步长为1。其中,所有MarkRange(整个数据片段)的最大数值区间为[A000, +inf],其完整的示意图如图6-11所示。

对索引的查询过程其实就是两个数值区间的交集判断,其中,一个区间是由基于主键的查询条件转化而来的条件区间;而另一个区间是刚才所讲述的与MarkRange对应的数值区间。
整个索引查询过程可以大致分为3个步骤。
-
生成查询条件区间:首先,将查询条件转化为条件区间。即便是单个值的查询条件,也会被转换成区间的形式。例如下面的例子。
WHERE ID = ‘A003’
[‘A003’, ‘A003’]
WHERE ID > ‘A000’
(‘A000’, +inf)WHERE ID < ‘A188’
(-inf, ‘A188’) -
递归交集判断:以递归的形式,依次对MarkRange的数值区间与条件区间做交集判断。从最大的区间[A000, +inf]开始:
- 如果不存在交集,则直接通过剪枝算法优化此整段MarkRange
- 如果存在交集,且MarkRange步长大于8(end - start),则将此区间进一步拆分成子区间
- 如果存在交集,且MarkRange不可再分解(步长小于8),则记录MarkRange并返回
-
合并MarkRange区间:将最终匹配的MarkRange聚在一起,合并它们的范围。
完整逻辑的示意图如图6-12所示。

MergeTree通过递归的形式持续向下拆分区间,最终将MarkRange定位到最细的粒度,以帮助在后续读取数据的时候,能够最小化扫描数据额范围。
四、二级索引
除了一级索引之外,MergeTree同样支持二级索引。二级索引又称跳数索引,由数据的聚合信息构建而成。我理解二级索引的字段和一级索引是不一样的。
4.1 granularity与index_granularity的关系
不同的跳数索引之间,除了它们自身独有的参数之外,还都共同拥有granularity参数。初次接触时,很容易将granualrity与index_granularity的概念弄混淆。对于跳数索引而言,index_granularity定义了数据的粒度,而granularity定义了聚合信息汇总的粒度,即隔几个index_granularity生成一条汇总信息。
以minmax索引为例,它的聚合信息是在一个index_granularity区间内数据的最小和最大极值。以下图为例,假设index_granularity=8192且granularity=3,则数据会按照index_granularity划分为n等份,MergeTree从第0段分区开始,依次获取聚合信息。当获取到第3个分区时(granularity=3),则汇总并会生成第一行minmax索引(前3段minmax极值汇总后取值为[1, 9]),如下图6-13所示。

4.2 跳数索引类型
- minmax:略
- set:略
- ngrambf_v1:ngrambf_v1记录的是数据短语的布隆过滤器,只支持String和FixedString数据类型。ngrambf_v1只能够提升in、notIn、like、equals和notEquals查询的性能。
- tokenbf_v1:tokenbf_v1索引是ngrambf_v1的变种,同样也是一种布隆过滤器。tokenbf_v1除了短语的处理方法外,其他与ngrambf_v1是完全一样的。tokenbf_v1会自动按照非字符的、数字的字符串分割token。
五、数据存储
5.1 压缩数据块
在MergeTree中,数据是以压缩数据块的形式被组织并写入.bin文件中的。压缩数据块好比一本书的文字段落,是组织文字的基本单元。
一个压缩数据块由头信息和压缩数据两部分组成。头信息固定使用9字节表示,具体由1个UInt8(1字节)整形和2个Unit32(4字节)整形组成,分别代表使用的压缩算法类型、压缩后的数据大小和压缩前的数据大小,具体如图6-14所示。

每个压缩数据块的体积,按照其压缩前的数据字节大小,都被控制在6KB-1MB,其上下限分别有min_compress_block_size(默认65536)与max_compress_block_size(默认1048576)参数指定。而一个压缩数据块最终的大小,则和一个间隔(index_granularity)内的数据实际大小有关。
MergeTree在数据具体写入过程中,会按照索引粒度(默认8192行),按批次获取数据并进行处理。如果把一批数据的未压缩大小设为size,则整个写入过程遵循以下规则:
1. **单个批次数据size < 64KB**:如果单个批次数据小于64KB,则继续获取下一批数据,直至积累到size>=64KB时,生成下一个压缩数据块。
1. **单个批次数据64KB<=size<=1MB**:如果单个批次数据大小恰好在64KB与1MB之间,则直接生成下一个压缩数据块。
1. **单个批次数据size>1MB**:如果耽搁批次数据直接超过1MB,则首先按照1MB大小截断并生成下一个压缩数据块。剩余数据继续按照上述规则执行。(这里我理解如果某一列,没一行数据都很大时,导致频繁超过1MB,我们就应该减小索引粒度)
整个逻辑过程如图6-15所示。

在.bin文件中引入压缩数据块的目的至少有以下两个:
1. 虽然数据被压缩后能够有效减少数据大小,降低存储空间并加速数据传输效率,但数据的压缩和解压动作,其本身也会带来额外的性能损耗。**所以需要控制被压缩数据的大小,以求在性能损耗和压缩率之间寻求一种平衡。**
1. 在读取某一列数据时(.bin文件),首先需要将压缩数据加载到内存并解压,这样才能进行后续数据处理。**通过压缩数据块,可以在不读整个.bin文件的情况下将读取粒度降低到压缩数据块级别,从而进一步缩小数据读取范围。**
六、数据标记
如果把MergeTree必做一本书,primary.idx一级索引好比这本书的一级章节目录,.bin文件中的数据好比这本书中的文字 ,那么数据标记(.mrk)就是一级章节目录和具体文字之间的关联。
对于数据标记而言,它记录了两点重要信息:是一级章节对应的页码信息;其二,是一段文字在某一页中的起始位置信息。这样一来,通过数据标记就能够很快地从一本书中立即翻到所关注内容所在的那一页,并知道从第几行开始阅读。
6.1 数据标记的生成规则
数据标记作为衔接一级索引和数据的桥梁,其像极了做过标记小抄的书签,而且书本中每个一级章节都拥有各自的书签。它们的关系如图6-17所示。

从图6-17中一眼就能发现数据标记的首个特征,即数据标记和索引区间是对齐的,均按照index_granularity的粒度间隔。如此一来,只需简单通过索引区间的下标编号就可以直接找到对应的数据标记。
为了能够与数据衔接,数据标记文件也与.bin文件一一对应。即每一个列字段[Column].bin文件都有一个与之对应的[Column].mrk数据标记文件,用于记录数据在.bin文件中的偏移量信息。
一行标记数据使用一个元组表示,元组内包含两个整型数值的偏移量信息。它们分别表示在此段数据区间内,在对应的.bin压缩文件中,压缩数据块的起始偏移量;以及将该数据压缩块解压后,其未压缩数据的起始偏移量。

如图6-18所示,每一行标记数据都表示了一个片段的数据(默认8192行)在.bin压缩文件中的读取位置信息。
6.2 数据标记工作方式
MergeTree在读取数据时,必须通过标记数据的位置信息才能找到所需要的数据。整个查找过程大致可以分为读取压缩数据块和读取解压数据两个步骤。
1. **读取压缩数据块**:查询某一列数据时,MergeTree无需一次性加在整个.bin文件,而是根据需要,只加在特定的压缩数据块。而这项特性需要借助标记文件中所保存的压缩文件中的偏移量。
2. **读取解压数据**:在读取解压后的数据时,MergeTree并不需要一次性扫描整段解压数据,它可以根据需要,以index_granularity的粒度加载特定的一小段。为了实现这项特性,需要借助标记文件中保存的解压数据块中的偏移量。
6.3 数据标记和压缩数据块的对应关系
由于压缩数据块的划分,与一个间隔(index_granularity)内的数据大小相关,每个压缩数据块的体积都被严格控制在64KB-1MB。而一个索引间隔的数据,只会产生一行数据标记。那么根据一个间隔内数据的实际字节大小,数据标记和压缩数据块之间会产生三种不同的对应关系。
6.3.1 多对一
多个数据标记对应一个压缩数据块,当一个间隔内的数据未压缩大小size小于64KB时,会出现这种对应关系。如图6-22所示

6.3.2 一对一
一个数据标记对应一个压缩数据块,当一个间隔内的数据未压缩大小size大于等于64KB且小于等于1MB时,会出现这种对应关系。如图6-23所示。

6.3.3 一对多
一个数据标记对应多个压缩数据块,当一个间隔内的数据未压缩大小size直接大于1MB时,会出现这种情况。如图6-24所示,编号45的标记对应了2个压缩数据块。

355

被折叠的 条评论
为什么被折叠?



