本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
在上一篇文章【ClickHouse】通过开心消消乐更新和删除数据|原来是用这种方式解决的吗中,我们接触到了MergeTree中的分区Partition
分区
用快递驿站做类比
快递包裹根据一定的分类和排序堆放在货架上
我家楼下就有一个菜鸟驿站
里面有好几个货架,每个货架上都有一个数字表示这是几号货架
每个货架上都分了好几层,每一层也有一个编号
货物按照上小下大的规则堆放在货架的每一层上
MergeTree中的分区也是根据一定的规则将数据存储在不同的文件中
我们可以在建表的时候用partition by来指定分区键
sql
create table express_delivery
(
id String comment '序号',
type Int8 comment '小件1,中件2,大件3',
consignor String comment '寄件人',
consignee String comment '收件人',
phone String comment '手机号',
storage_time DateTime comment '入库时间'
) engine = MergeTree()
order by storage_time
partition by (toInt64(phone) % 3 + 1, type)
comment '只有三个货架的快递驿站';
创建一张表存储快递信息(省略其他字段)
假设我们的快递驿站只有3个货架
先使用手机号对3取余数来确定货架的编号
然后根据快递是小件,中件,大件来确定层号
最后根据入库时间进行排序
(以上只是一种假设,并非实际的算法)
所以我们的分区键就可以用partition by (toInt64(phone) % 3 + 1, type)来确定
现在有一个快递到了
sql
/* A先生有一个小件快递 */
insert into express_delivery(id, type, consignor, consignee, phone, storage_time)
values ('0001', 1, 'A先生', 'F先生', '18312340001', now());
我们来查看一下分区的信息
sql
SELECT partition, name, rows, active FROM system.parts WHERE table = 'express_delivery';
可以查到下列数据
| partition | name | rows | active |
|---|---|---|---|
| (3,1) | 3-1_1_1_0 | 1 | 1 |
其中partition=(3,1)就是第一条数据所在的分区
现在又有两个快递到了
sql
/* B先生有一个小件快递 */
insert into express_delivery(id, type, consignor, consignee, phone, storage_time)
values ('0002', 1, 'B先生', 'F先生', '18312340002', now());
/* A先生有一个大件快递 */
insert into express_delivery(id, type, consignor, consignee, phone, storage_time)
values ('0003', 3, 'A先生', 'F先生', '18312340001', now());
我们来看下现在的分区
| partition | name | rows | active |
|---|---|---|---|
| (1,1) | 1-1_2_2_0 | 1 | 1 |
| (3,1) | 3-1_1_1_0 | 1 | 1 |
| (3,3) | 3-3_3_3_0 | 1 | 1 |
(1,1)的分区是B先生的小件快递
(3,3)的分区是A先生的大件快递
叮!A先生收到了一条短信(bushi:
【CK驿站】凭 3-1-0001,3-3-0003 到 express_delivery 取件。
当我们查询的时候,如果满足一定条件,就能够直接找到对应的数据文件进行读取
就像我们在取快递的时候,能够根据取件码直接锁定货架和层数
合并
最近A先生网购比较频繁,又到了两个快递
sql
/* A先生有一个中件快递 */
insert into express_delivery(id, type, consignor, consignee, phone, storage_time)
values ('0004', 2, 'A先生', 'F先生', '18312340001', now());
/* A先生有一个中件快递 */
insert into express_delivery(id, type, consignor, consignee, phone, storage_time)
values ('0005', 2, 'A先生', 'F先生', '18312340001', now());
我们来看一看现在的分区(之前的分区就不展示了)
| partition | name | rows | active |
|---|---|---|---|
| (3,2) | 3-2_4_4_0 | 1 | 1 |
| (3,2) | 3-2_5_5_0 | 1 | 1 |
???同一个分区怎么会有两条数据?
毕竟按我们目前对分区的了解
应该是只有一条(3,2)的分区数据并且该分区中有两条记录
还记得我们的表引擎叫什么吗?
MergeTree合并树
既然叫合并树,自然需要合并一些东西
没错,需要合并的就是分区
为什么要合并(猜想)
应该是考虑到插入效率的问题所以才会将两次插入区分开来
假设我们不想要额外的合并操作
直接把第二条数据追加到第一条数据之后会有什么问题呢
首先就是并发的问题
第二条数据插入的时候发现第一条数据还没有插入完
那是不是要等第一条数据插入完成才执行第二条数据的插入
虽然我们这里的第一条数据只有一条
但是实际情况可能有几万条十几万条
这个等待的时间就不太合适
假设可以通过一些技术让两次插入可以同时执行也不出错
但是我们指定了排序键
这样就需要在两次插入之后再进行一次排序
如果整个插入操作都是同步的话
这个插入效率就太低了
所以不如每次都写入新的文件
然后在空闲的时候将这两个文件合并成一个新的文件
这样既能保证插入的性能又能利用空闲时候的资源来合并数据
怎么合并
一般来说,ClickHouse会在数据插入之后的十几二十分钟后自动合并分区
我们再来看一下分区(一段时间之后)
| partition | name | rows | active |
|---|---|---|---|
| (3,2) | 3-2_4_5_1 | 2 | 1 |
两个分区数据合并成的一个,数据也变成的两行
不知道大家有没有发现
合并之后name字段由原来的3-2_4_4_0和3-2_5_5_0变成了3-2_4_5_1
那么后面这一串的数字是什么呢
我们来回忆一下之前的所有分区数据(按插入顺序)
| partition | name | rows | active |
|---|---|---|---|
| (3,1) | 3-1_1_1_0 | 1 | 1 |
| (1,1) | 1-1_2_2_0 | 1 | 1 |
| (3,3) | 3-3_3_3_0 | 1 | 1 |
| (3,2) | 3-2_4_4_0 | 1 | 1 |
| (3,2) | 3-2_5_5_0 | 1 | 1 |
可以看到除了最前面的部分和分区一样,后面开始的2个数字是递增的
也就是说下一次插入会是分区_6_6_0
sql
insert into express_delivery(id, type, consignor, consignee, phone, storage_time)
values ('0006', 2, 'A先生', 'F先生', '18312340001', now());
看看分区的数据(忽略之前的分区数据)
| partition | name | rows | active |
|---|---|---|---|
| (3,2) | 3-2_6_6_0 | 1 | 1 |
果然没错,每次插入数字都会递增
那么当两个分区合并的时候这几个数字又是根据什么规则来定的呢
第一个数字会取小一点的那一个
第二个数字会取大一点的那一个
第三个数字会取大一点的那一个再加1,可以看作是合并的次数
所以之前3-2_4_4_0和3-2_5_5_0合并时
第一个数字取4和5中小一点的4
第二个数字取4和5中大一点的5
第三个数字取0和0中大一点的0再加1就是1
所以就变成了3-2_4_5_1
那么我们现在就可以预测3-2_4_5_1和3-2_6_6_0合并之后这几个数字会是多少
第一个数字取4和6中小一点的4
第二个数字取5和6中大一点的6
第三个数字取1和0中大一点的1再加1就是2
所以合并之后应该是3-2_4_6_2
手动触发一下分区合并
sql
optimize table express_delivery partition (3,2);
再来看分区数据
| partition | name | rows | active |
|---|---|---|---|
| (3,2) | 3-2_4_6_2 | 3 | 1 |
和我们预测的一样,合并之后数据也变成了3行
总结
ClickHouse的MergeTree表引擎
在每次插入的时候都会写入新的分区文件
在空闲的时候再将相同分区的数据合并
另外需要注意在指定分区键的时候不要让生成的分区过多,会影响性能
比如按照时间分区的时候可以按年或月来分区,不要按日或小时来分区