【ClickHouse】数据们的分分合合

本文为稀土掘金技术社区首发签约文章,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_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

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_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

手动触发一下分区合并

sql 复制代码
optimize table express_delivery partition (3,2);

再来看分区数据

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

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

总结

ClickHouseMergeTree表引擎

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

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

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

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

相关推荐
爱的叹息12 分钟前
Spring Boot中事务状态(TransactionStatus)的核心信息及常见应用场景
java·spring boot·后端
安然无虞13 分钟前
31天Python入门——第20天:魔法方法详解
开发语言·后端·爬虫·python
锋行天下40 分钟前
WebSocket 即时通讯前后端设计和基于token的鉴权
前端·后端
猿java1 小时前
程序员,你使用过灰度发布吗?
java·分布式·后端
iOS开发上架哦1 小时前
Flutter,让我们把 Navigator与Route详解 再讲一遍
后端
半桔1 小时前
红黑树剖析
c语言·开发语言·数据结构·c++·后端·算法
疯狂的程序猴1 小时前
flutter - 图文讲解表单组件基本使用 & 注册实战
后端
星星电灯猴1 小时前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
后端
Asthenia04121 小时前
深入剖析 MyBatis-Plus 自动注入封装的实现原理及其创新
后端
佩奇快跑1 小时前
使用 Redis Stream 解决 Java 与 Python 的长连接请求交互
后端