【ClickHouse】数据们的分分合合


theme: channing-cyan

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

在上一篇文章【ClickHouse】通过开心消消乐更新和删除数据|原来是用这种方式解决的吗中,我们接触到了MergeTree中的分区Partition

分区

用快递驿站做类比

快递包裹根据一定的分类和排序堆放在货架上

我家楼下就有一个菜鸟驿站

里面有好几个货架,每个货架上都有一个数字表示这是几号货架

每个货架上都分了好几层,每一层也有一个编号

货物按照上小下大的规则堆放在货架的每一层上

MergeTree中的分区也是根据一定的规则将数据存储在不同的文件中

我们可以在建表的时候用partition by来指定分区键

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)来确定

现在有一个快递到了

/* A先生有一个小件快递 */
insert into express_delivery(id, type, consignor, consignee, phone, storage_time)
values ('0001', 1, 'A先生', 'F先生', '18312340001', now());

我们来查看一下分区的信息

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)就是第一条数据所在的分区

现在又有两个快递到了

/* 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先生网购比较频繁,又到了两个快递

/* 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_03-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

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_03-2_5_5_0合并时

第一个数字取45中小一点的4

第二个数字取45中大一点的5

第三个数字取00中大一点的0再加1就是1

所以就变成了3-2_4_5_1

那么我们现在就可以预测3-2_4_5_13-2_6_6_0合并之后这几个数字会是多少

第一个数字取46中小一点的4

第二个数字取56中大一点的6

第三个数字取10中大一点的1再加1就是2

所以合并之后应该是3-2_4_6_2

手动触发一下分区合并

optimize table express_delivery partition (3,2);

再来看分区数据

partition name rows active
(3,2) 3-2_4_6_2 3 1

和我们预测的一样,合并之后数据也变成了3行

总结

ClickHouseMergeTree表引擎

在每次插入的时候都会写入新的分区文件

在空闲的时候再将相同分区的数据合并

另外需要注意在指定分区键的时候不要让生成的分区过多,会影响性能

比如按照时间分区的时候可以按年或月来分区,不要按日或小时来分区


这是一个从 https://juejin.cn/post/7368836713965387815 下的原始话题分离的讨论话题