文法约定

  • ig:index_granularity
  • igb:index_granularity_bytes

MergeTree 相关概念

MergeTree 建表

CREATE TABLE t_foo (
   ...
)
ENGINE = MergeTree
ORDER BY (a1, a2, a3)
PRIMANY KEY a1, a2
PARTITION BY xxx
SAMPLE By yyy
SETTINGS n1=v1, n2=v2, n3=v3 ...

排序键和主键

建表必选指定排序健(ORDER BY)和主键(PRIMANY KEY)中的至少一个。如果只指定其中一个,等价于它们二者相同。

排序键表示,数据在底层存储上是按什么排序的。它可以是某列,也可以是表达式。

主键必须是排序键的前缀(包括与排序键相同)。例如,排序键是

a1, a2, a3

则主键可以是下列各项中的任何一个:

a1
a1, a2
a1, a2, a3

分区键

如果指定了分区键,则表中的数据会按照分区键存入不同的目录中(一个分区对应至少一个目录,后述)。如果不指定分区键,则会创建一个名为 all 的分区。
用 PARTITION BY 指定分区键。
它可以是一个表达式,例如,表中某字段 collection_time 类型为 DateTime,则可以设置分区键为 toYYYYMM(collection_time) ,这样就实现了按月分区。

采样键

通过 SAMPLE BY 指定采样键。它的作用是进行查询

SELECT xxx FROM t_foo SAMPLE 0.1

的时候,按比例(这里是 0.1 也就是 10%)进行抽样。
例如,建表时,SAMPLE BY intHash32(user_id) 则会筛选出满足

intHash32(user_id) < INT32_MAX * 0.1

的行,所以,当作采样键的表达式必须要比较“匀”(例如 hash),才能达到随机采样的效果。

SETTINGS 索引颗粒度

index_granularity(记为 ig)指的是,每 ig 行生成一条索引,默认 8192。
index_granularity_bytes(记为 igb)指的是,每经过 igb 字节的存储空间生成一条索引,默认 10485760(10 MiB)。相比于按照 ig 生成索引,它可以根据行的大小自适应。如果设为 0 则表示不启用它,直接按 ig 走,也就是关闭自适应。

data part

在默认情况下:

库对应目录(简称库目录);表对应库的直接子目录(简称表目录);表的分区对应表目录下面的若干子目录(注意,分区对应一族目录而不是一个目录)。

例如,库 default 中有表 t_foo,该表中有分区 202208202209,则目录情况形如:

<DATA>/default/t_foo/202208_1_1_0
<DATA>/default/t_foo/202208_3_3_0
<DATA>/default/t_foo/202209_2_2_0

上面的 202208_xxx 就是 202208 这个分区对应的目录们。其中每一个目录称为属于该分区的 data part。

ClickHouse 每成功执行一次 INSERT 语句(哪怕只插一行)都会产生一个 data part,一个 data part 一旦生成完毕,就不再可变(除了被删)。ClickHouse 会择机合并(merge)同一个分区的 data part:基于多个现有的 data part 产生一个新的 data part,新 data part 生成完毕后,旧 data part(s) 会被择机删除。

这就是 MergeTree 中 merge 的含义。

不可变的 data part,这一设计使得很多问题变得简单,例如,不用考虑并发安全性,因为不可变的东西一定并发安全。

从上面的叙述可以看出,ClickHouse 不可以频繁 INSERT,因为频繁 INSERT 会导致大量的 data part,导致 data part 合并的速度赶不上生成的速度,进而导致过多的文件被打开,从而让系统的打开文件数到达上限(Linux)。

MergeTree 的存储结构

在每一个 data part 目录中,[column].bin 存储各列的数据,[column].mrk(使用 ig 时)或 [column].mrk2(使用 igb 时)表示 mark 信息。如果表中的数据很少,则只有 data.bin 和 data.mrk3,此时不分列存储,所有数据存在一起。
还有 primary.idx 主键索引,minmax_xxx.idx 分区键索引。

颗粒、block、mark

mark 信息涉及两个概念:颗粒(granule)和块(block)。

颗粒

颗粒是个逻辑概念,若干行算一个颗粒,如果启用了 igb,则 igb 字节算一个颗粒,否则 ig 行算一个颗粒。如果按 ig,则表非常宽的时候,颗粒会变得很大。
颗粒是 ClickHouse 在内存中进行数据扫描的最小单位。

block

block 是列存文件 [column].bin 中的压缩单元,每个列存文件中包含 min_compress_block_size 个颗粒,当 block 中每写完一个颗粒时,会检查颗粒数到没到 min_compress_block_size,到了就把 block 压缩写到硬盘。

mark

每个字段在每个 data part 中有一个 [column].mrk2 文件。

mark 文件的作用是:
1)记录每个颗粒的行数;
2)记录上述颗粒所在的 block 在列存文件 [column].bin 中的偏移(简写 do);
3)记录颗粒在解压后的 block 中的偏移(简写 co)。

它可以看成一个表格

颗粒行数

do

co

备注

8192

48000

14800

0 号颗粒的信息

7985

96487

25877

1 号颗粒的信息

备注是我加的,mark 文件中没有。

索引

主键索引

主键索引存储的是每一个颗粒的首行的主键的值。

分区键索引

MergeTree 存储会统计每个 Data Part 中分区键的最大值和最小值,当用户查询中包含分区键条件时,就可以直接排除掉不相关的 Data Part。
事实上,分区(partition)也不是物理概念,而是逻辑概念,data part 才是物理概念,一个分区会有多个 data part,每次 insert(哪怕只插入一条)都会生成一个 data part,然后属于同一个分区的 data part 会择机进行合并,这也是 MergeTree 中 merge 的含义。

主键索引的用途

在查询的时候,如果查询条件是主键,则先找到主键索引,通过取交集可知要查询的数据在第多少个颗粒中:

假设查询为

SELECT aa, bb FROM t_foo
WHERE collection_time >= a AND collection_time <= b

而主键索引的存储内容为(示意)

v0, v1, v2, ... v_{n-1}

假设 v[i] 是第一个满足 v[i+1] >= a 的,v[j] 是最后一个满足 v[j+1] >= b 的,那么从 v[i] 到 v[j] 对应的颗粒都可能包含要查的数据。

然后,通过 mark 文件找到这些颗粒的 do 和 co,do 用来定位 block 在 bin 中的位置,co 用来定位颗粒在 block 中的位置,然后就可以把颗粒读入内存用来筛选。

分区键索引的用途

在每个 data part 目录中,有 minmax_xxx.idx,这就是分区键索引。它里面保存着该 data part 的分区键的最小、最大值。

当查询条件包括分区键时,会读取分区键索引,这样一来,就可以不读没必要读的 data part。