ClickHouse存储引擎之MergeTree引擎——数据存储

一、按列存储

​ 在MergeTree中,数据按列存储,每个字段也独立单独存储,每个列字段均拥有一个对应的column.bin数据文件,这些数据文件便为数据的物理存储。数据文件以分区目录的形式被组织存放,所以每个分区目录中的bin文件只保存了当前分区片段内的该列数据。按列独立存储有利于更好的进行数据压缩(相同类型数据存放在一起),还可以最小化需扫描数据的范围。

​ MergeTree会将经过压缩的数据存放到对应column.bin文件中,默认使用LZ4算法,然后将数据按照声明的ORDER BY排序。最后,数据会以压缩数据块的方式被有序的写入数据文件中的。

二、压缩数据块

1、简介

​ 一个压缩数据块由头信息和压缩数据两部分组成。头信息固定使用9位字节表示,具体由1个UInt8(1字节)和2个UInt32(4字节)整型组成,分别代表了使用的压缩算法类型、压缩后的数据大小和压缩前的数据大小,具体格式如图所示:

​ 从上图可以看出,column.bin文件由多个压缩数据块组成,每个压缩数据块的头部信息是基于CompressionMethod_CompressedSize_UncompressedSize公式生成,可通过ClickHouse提供的clickhouse-compressor工具查询到某个.bin文件中压缩数据的统计信息。这里以官方提供的测试数据hit_v1为例,执行该命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
clickhouse-compressor --stat <  /var/lib/clickhouse/data/datasets/hits_v1/201403_1_32_2/JavaEnable.bin 
## 结果如下,每一行数据表示一个压缩数据块的头信息,分表表示该压缩数据块中未压缩数据大小和压缩后数据大小(打印信息和物理存储的顺序刚好相反)
65536 12000
65536 14661
65536 4936
65536 7506
65536 18660
65536 14892
65536 17474
65536 13464
65536 14999
...
72776 1546
68558 12639
72089 11184
71612 6945
65857 11135
7963 1559

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
clickhouse-server_1 :) select * from system.settings where name like '%_compress_block_size%'\G

SELECT *
FROM system.settings
WHERE name LIKE '%_compress_block_size%'

Row 1:
──────
name: min_compress_block_size
value: 65536
changed: 0
description: The actual size of the block to compress, if the uncompressed data less than max_compress_block_size is no less than this value and no less than the volume of data for one mark.
min: ᴺᵁᴸᴸ
max: ᴺᵁᴸᴸ
readonly: 0

Row 2:
──────
name: max_compress_block_size
value: 1048576
changed: 0
description: The maximum size of blocks of uncompressed data before compressing for writing to a table.
min: ᴺᵁᴸᴸ
max: ᴺᵁᴸᴸ
readonly: 0

2 rows in set. Elapsed: 0.002 sec.

2、压缩规则及流程

​ MergeTree在数据存储过程中,会遵循以下规则:

  • 单个索引粒度间隔数据size < 64KB:如果单个索引粒度数据大小小于64KB,则继续获取下一个索引粒度的数据,一直到size >= 64KB,生成下一个压缩数据块。
  • 单个索引粒度间隔数据 64KB <= size <= 1MB:如果单个索引粒度数据大小大于64KB,小于1MB,则直接生成下一个压缩数据块
  • 单个索引粒度间隔数据 size > 1MB:如果单个索引粒度数据大小超过1MB,则先按照1MB大小截断并生成下一个压缩数据块,剩余数据按照这三个规则对应执行。这时就会出现一批数据生成多个压缩数据块的情况。

3、总结

​ 一个column.bin文件是由一个到多个压缩数据块组成的,每个压缩数据块大小在64KB~1MB之间。多个压缩数据块之间,按照写入顺序首尾相接,紧密排列在一起。数据被压缩后可以减少数据大小,降低存储空间并且加快数据的传输效率,但数据的压缩和解压动作,本身也会带来额外的性能损耗,所以需要控制被压缩数据的大小。另外,在具体读取某一列的压缩数据时,首先需要将压缩数据(包含了整个压缩数据块以及下个压缩数据块的头文件)加载到内存并解压,再进行后续的数据处理。通过压缩数据块,可以在不读取整个.bin文件的情况下将读取粒度降低到压缩数据块级别,进一步缩小了数据读取的范围。