MergeTree原理详解之数据存储

前面我们讲解了MergeTree引擎索引的原理,但是仅仅依靠索引,并不能支撑ClickHouse如此强悍的性能。这篇文章将为你解决以下问题,数据在底层具体是如何存储,怎么根据索引编号找到对应的数据。

列独立存储

说到存储,大部分MPP数据库都是用的同一种思想,即列式存储。ClickHouse也不例外。
在MergeTree中,数据按照列存储,注意哦,是完全列式存储,每个列字段都拥有一个与之对应的.bin文件,这些文件承载着数据的物理存储。数据文件以分区目录的形式被组织存储,在bin文件中只会保存当前分区片段内的这部分数据。

这样存有以下优点:

  1. 可以更好的进行数据压缩,相同类型的数据放在一起,对压缩更加友好
  2. 能够最小化数据扫描的范围。

MergeTree将数据写入.bin文件之前需要经过以下处理:
3. 数据需要经过压缩,默认是LZ4算法
4. 数据会事先按照order by的声明排序
5. 数据是以压缩数据块的形式被组织并写入.bin文件的

压缩数据块

.bin文件中有多个压缩数据块,每个压缩数据块的体积,按照其压缩前的数据字节大小,都被严格控制在64kb到1mb之间,分别由min_compress_block_size(默认64k)和max_compress_block_size(默认1M)指定。

组成部分:
一个压缩数据块由头部信息和压缩数据两部分组成。头部信息由压缩算法类型、压缩后的数据大小和压缩前的数据大小组成,分别为1个UInt8(1字节)整型和2个UInt32(4字节)整型组成。
在这里插入图片描述

压缩数据块的生成

MergeTree在数据写入的过程中,会按照索引粒度(每隔8192行),按批次获取数据进行处理。过程如下:

  1. 单个批次数据size<64kb:继续取下一批数据,直到累积size>=64kb时,生成下一个数据块。
  2. 单个批次数据64kb<size<=1MB:直接生成下一个压缩数据块
  3. 单个批次数据size>1MB:首先按照1MB大小截断并生成下一个压缩数据块。剩余数据继续依照上述规则执行。此时会出现一个批次数据生成多个压缩数据块的情况。

结论:一个bin文件是由一个或多个压缩数据块组成,每个压缩块的大小在64kb~1mb之间,多个数据块之间按照写入顺序首尾相接,紧密地排列在一起。且每个数据间隔与压缩数据块存在三种关系:一对多,一对一,多对一。

这么做有什么好处呢?
其实这是ClickHouse在性能损耗和压缩率之间的平衡,压缩后的数据能够有效减少数据大小,降低存储空间并加速数据传输效率,但是数据的压缩和解压缩本身也会带来额外的性能损耗,所以必须控制压缩数据块的大小。
其次在具体读取某一列数据的时候,首先要将数据加载到内存并解压,这样才能进行后续的数据处理,通过压缩数据块的方式,ClickHouse并不用把整个bin文件读取到内存中,只需读取bin文件中特定的压缩块即可,从而进一步缩小了数据读取的范围。

那么问题来了,ClickHouse是怎么根据索引找到指定的压缩块的呢?那就要用到我们接下来要讲的数据标记了。

数据标记

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

值得注意的是:标记数据和一级索引数据不同,它并不常驻内存,而是使用LRU(最近最少使用)存储策略加快其读取速度。
在这里插入图片描述

数据标记的生成

数据标记是用来连接索引和数据的,由前面我们知道,数据标记和索引区间是对齐的,均按照index_granularity的粒度间隔。这样的话,我们只要找到索引编号就能直接找到对应的数据标记。
在数据标记文件中(.mrk),一行标记使用一个元祖表示,(此数据区间在.bin压缩数据块的起始位置,未压缩数据的起始偏移位置)
如下图所示:
(1)在对应的.bin压缩文件中,压缩数据块的起始偏移量
(2)在对应的.bin压缩文件中,将该数据压缩块解压后的起始偏移量
在这里插入图片描述
上图中每一行数据标记与数据间隔(index_granularity)对应,假设两个数据间隔即16384行的大小恰好超过了64KB,此时根据数据块生成原则,生成了第一个压缩数据块,该压缩块的偏移量为[0,12016]。
那么这里有个问题,这里的12016就代表着数据块的大小吗?
其实并不是,前面我们说到压缩数据块包含两个部分,一部分是头文件,另一部分才是数据压缩。那么这12016就可以简单的用一下图表示:
在这里插入图片描述

压缩数据块读取数据流程

上面我们了解了压缩数据块的基本构造以及生成规则,下面我们再给大家讲讲压缩数据块读取数据的流程。
查找过程可以分为读取压缩数据块和读取数据两个步骤。
(1)读取压缩数据块:在查询某一列数据时,MergeTree无须一次性加载整个.bin文件,而是可以根据需要,只加载特定的压缩数据块。那么就得通过我们上面所讲的数据标记文件来实现。两个相邻的压缩数据块的起始偏移量构成了当前标记对应的压缩数据块的偏移量区间。例如上图,在读取时,就可以直接读取.bin文件的[0,12016]字节的数据,就可以获得第0个压缩数据块。
(2)读取数据:读取压缩数据块后,MergeTree也并不需要扫描整个解压数据。前面我们讲到数据标记文件的构造中就提到,每个一行代表一个索引区间,不但对应着压缩数据块的起始位置,还对应着解压后的数据块的起始偏移量,还是以上图为例,假如我们要查询的数据对应着索引区间1,那么此时我拿到压缩数据块0解压之后,只需从解压数据块的57344字节位置开始扫描。

分区、索引、标记、压缩数据块协同作用

写入过程

在上一篇MergeTree原理之索引中我们知道,伴随的每次的写入,都会对应生成分区目录。这些分区目录,在后续的某一时刻会依据规则进行合并,相同分区的目录会合并到一起。接着按照index_granularity(默认8192行)索引粒度,会分别生成primary.idx一级索引(如果声明了二级索引,还会创建二级索引文件)、每一个列字段的.mrk数据标记和.bin压缩数据文件。

查询过程

数据的查询就是一个不断减小范围的过程。理论上,最理想的情况下,MergeTree首先可以依次借助分区索引、一级索引和二级索引,将数据扫描范围缩至最小。然后借助数据标记,定位到需要进行计算的数据范围。
最不理想的情况下是,一条查询语句没有指定任何WHERE条件,或者WHERE条件没有使用到任何索引(分区索引、一级索引和二级索引),那么它会扫描所有分区目录,一级目录内索引段的最大区间。虽然没办法减少数据扫描的范围,但是MergeTree可以借助数据标记,以多线程的形式同时读取多个压缩数据块,以提升性能。

总结

结合上一章索引的原理,我们可以说对MergeTree进行了一个全方位的剖析,首先是MergeTree的基础属性和物理存储结构,还有数据分区、一级索引、二级索引、数据存储和数据标记的重要概念。然后介绍了它们是如何进行协同作用的。相信到这你已经对MergeTree引擎有一个比较全的了解了,在后面的文章中,我们将进行实操,动手用起来。

微信公众号:喜讯Xicent

image

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值