文法约定
- 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
,该表中有分区 202208
、202209
,则目录情况形如:
<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。