在MergeTree表引擎中,数据是以分区目录的形式进行组织的,每个分区独立分开存储,这样子在数据查询时候可以跳过无用的数据分区,最小化扫描的数据文件,从而达到加快查询的效果。本文将从分区的规则,底层目录的格式以及分区的合并规则等方面详细讲解数据分区目录具体是如何运作的。
分区规则
MergeTree数据表的分区规则由分区 ID 决定,而具体到每个分区对应的 ID 则是由分区键的取值决定的。分区键支持使用任何一个或一组字段表达式声明,其业务语义可以是年、月、日或者组织单位等任何一种规则。针对取值数据的类型不同,分区 ID 的生成逻辑目前拥有四种规则:
- 不指定分区键:如果不使用分区键,即不使用PARTITION BY声明任何分区表达式,则分区 ID 默认为all,所有的数据都会被写入 all 这个分区;
- 使用整型:如果分区键的取值为整型(UInt64、Int8 等等都算),且无法转成日期类型 YYYYMMDD 格式,则直接按照该整型的字符串形式作为分区 ID 的取值;
- 使用日期类型:如果分区键取值属于日期类型,或者是能够转换为 YYYYMMDD 格式的整型,则使用按照 YYYYMMDD 进行格式化后的字符串形式作为分区 ID 的取值;
- 使用其它类型:如果分区键取值既不是整型、也不是日期类型,比如 String、Float 等等。则通过 128 位 Hash 算法取其 Hash 值作为分区 ID 的取值。
分区目录格式
对于MergeTree来说,最核心的特点是其分区目录的合并动作,从分区目录的命名中便可解读出它的合并逻辑,一个完整的分区目录的命名公式如下所示:PartitionID_MinBlockNum_MaxBlockNum_Level
- PartitionID:分区 ID,根据分区ID生成规则生成;
- MinBlockNum、MaxBlockNum:最小数据块编号和最大数据块编号,这里的命名很容易让人联想到后面要说的数据压缩块,甚至产生混淆,但实际上这两者没有任何关系。这里的 BlockNum 是一个自增的整数,从 1 开始,每当创建一个新的分区时就会自增 1,并且对于一个新的分区目录而言,它的 MinBlockNum 和 MaxBlockNum 是相等的。比如 202005_1_1_0、202006_2_2_0、202007_3_3_0,以此类推;
- Level:合并的层级,可以理解为某个分区被合并的次数,这里的 Level 和 BlockNum 不同,它不是全局累加的。对于每个新创建的目录而言,其初始值都为 0,之后以分区为单位,如果相同分区发生合并动作,则该分区对应的 Level 加 1。
分区合并
MergeTree 的分区目录和其它传统意义上数据库有所不同,主要表现在以下两个方面:
- 首先 MergeTree 的分区目录并不是在数据表被创建之后就存在的,而是在数据写入的过程中被创建的,如果一张表中没有任何数据,那么也就不会有任何的分区目录,这是因为分区目录的命名与分区 ID 有关,而分区 ID 又和分区键对应的值有关,而表中连数据都没有,也就没有目录了;
- 其次,MergeTree 的分区目录也不是一成不变的,在其它数据库的设计中,追加数据的时候目录自身不会改变,只是在相同分区中追加数据文件。而 MergeTree 完全不同,伴随着每一次数据的写入,MergeTree 都会生成一批新的分区目录,即使不同批次写入的数据属于相同的分区,也会生成不同的分区目录。也就是说对于同一个分区而言,会存在对应多个分区目录的情况。而在之后的某个时刻(一般 10 到 15 分钟),ClickHouse 会通过后台任务将属于相同分区的多个目录合并(Merge)成一个新的目录,当然也可以通过 optimize TABLE table_name FINAL 语句立即合并,至于合并之前的旧目录会在之后的某个时刻(默认 8 分钟)被删除。
分区合并的规则
分区合并是clickhouse异步对数据合并并进行压缩,主要为了节省空间,属于同一个分区的多个目录,在合并之后会生成一个全新的目录,目录中的索引和数据文件也会相应地进行合并。而新目录的名称的生成方式遵循如下规则:
- PartitionID:不变;
- MinBlockNum:取同一分区内所有目录中最小的 MinBlockNum;
- MaxBlockNum:取同一分区内所有目录中最大的 MaxBlockNum;
- Level:取同一分区内最大 Level 值并加 1。
分区合并实例
我们通过一个实例来详细看一下分区合并,假设我们之前的有一个partition_v5
的表,按照日期字段格式分区(Partition by toYYYYMM(EventTime)
:
T0时刻插入数据:
INSERT INTO partition_v5 VALUES (1, '张三', '2020-05-01');
INSERT INTO partition_v5 VALUES (1, '李四', '2020-05-02');
INSERT INTO partition_v5 VALUES (1, '王五', '2020-06-01');
T2时刻插入数据:
INSERT INTO partition_v5 VALUES (1, '张三', '2020-05-01');
INSERT INTO partition_v5 VALUES (1, '李四', '2020-05-02');
INSERT INTO partition_v5 VALUES (1, '王五', '2020-06-01');
- 在T0时刻,我们分3 批写入 3 条数据,根据规则,ClickHouse 会创建 3 个分区目录,分区目录的 PartitionID 部分依次为 202005、202005、202006;而对于每个新创建的分区目录而言,它们的 MinBlockNum 和 MaxBlockNum 都是相等的,并且我们说 MinBlockNum 和 MaxBlockNum 是全局的,从 1 开始自增,所以三个分区目录的 MinBlockNum 和 MaxBlockNum 依次是 1_1、2_2、3_3;最后是 Level,每个新建的分区目录的初始 Level 都是0。因此三个分区目录的最终名称就是 202005_1_1_0、202005_2_2_0、202006_3_3_0;
- 在T1时刻,MergeTree 的合并动作开始了,那么属于同一分区的 202005_1_1_0、202005_2_2_0 将会发生合并,得到 202005_1_2_1(MinBlockNum 取最小值、MaxBlockNum 取最大值,Level 去最大值加 1);
- 在T2时刻,我们再分 3 批写入插入三条数据,时间分别为 2020-05-03、2020-06-02、2020-07-01,那么会再创建 3 个分区目录,分区 ID 分别为 202005、202006、202007;
- 之后在T3时刻,MergeTree 的合并动作开始,属于相同分区的目录开始合并,202005_1_2_1 和 202005_4_4_0 会发生合并,得到 202005_1_4_2;202006_3_3_0 和 202006_5_5_0 发生合并,得到 202006_3_5_1。
如果再写入数据的话,那么 MergeTree 依旧会发生合并,然后重复和上面的一样的动作。我们上面显示的是目录合并之后的结果,至于旧的分区目录、也就是合并之前的目录会依旧保留一段时间,但已不再是激活状态(active = 0),在数据查询的时候会被过滤掉。然后 ClickHouse 有一个后台任务会定时扫描(默认 8 分钟),负责将 active = 0 的目录从物理磁盘上删除。
最后,我们用一张完整的示例图作为总结,描述MergeTree分区目录从创建,合并到删除的整体过程:
分区相关命令
通过系统表查看分区状态
SELECT
partition,
name,
active
FROM system.parts
WHERE table = 'xx' and database ='xx';
系统表属性解析如下:
- partition 列存储分区的名称。此示例中有1个分区:1
- name 列为分区目录的名称;
- part_type 数据存储格式。Wide: 每一列都单独存储一个数据文件;Compact: 所有列都存储一个数据文件;
- active 列为片段状态。1 代表激活状态;0 代表非激活状态。非激活片段是那些在合并到较大片段之后剩余的源数据片段。损坏的数据片段也表示为非活动状态。
分区删除时间设置
但是有些特殊场景下,用户希望立刻删除老的数据目录,这种需求可以在创建MergeTree表的时候通过Settings属性来设置,old_parts_lifetime 这个属性的含义是多久删除老的分区数据目录。单位是秒
DETACH PARTITION|PART
ALTER TABLE table_name DETACH PARTITION|PART partition_expr
将指定分区的所有数据移动到detached
目录中,服务器忘记了分离的数据分区,就好像它不存在一样。在进行ATTACH查询之前,服务器不会知道此数据。
ALTER TABLE mt DETACH PARTITION '2020-11-21';
ALTER TABLE mt DETACH PART 'all_2_2_0';
执行查询后,你可以对detached
目录中的数据做任何你想做的事情,从文件系统中删除它,或者直接离开它。它将数据移动到detached
所有副本上的目录。请注意,只能在领导者副本上执行此查询,要确定副本是否是领导者,请对system.replicas表执行SELECT
查询。
DROP PARTITION|PART
ALTER TABLE table_name DROP PARTITION|PART partition_expr
从表中删除指定的分区。此查询将分区标记为非活动并完全删除数据,大约在 10 分钟内。
DROP DETACHED PARTITION|PART
ALTER TABLE table_name DROP DETACHED PARTITION|PART partition_expr
删除指定分区的指定数据块或所有数据块detached
。
ATTACH PARTITION|PART
ALTER TABLE table_name ATTACH PARTITION|PART partition_expr
将数据从detached
目录添加到表中。可以为整个分区或单独的数据块添加数据。例子:
ALTER TABLE visits ATTACH PARTITION 201901;
ALTER TABLE visits ATTACH PART 201901_2_2_0;
副本发起者检查目录中是否有数据detached
。如果数据存在,则查询检查其完整性。如果一切正确,则查询将数据添加到表中。如果接收到附加命令的非发起者副本在其自己的detached
文件夹中找到具有正确校验和的数据块,它会附加数据而不从其他副本中获取数据。如果没有具有正确校验和的零件,则从具有该零件的任何副本下载数据,可以将数据detached
放在一个副本上的目录中,并使用ALTER ... ATTACH
查询将其添加到所有副本上的表中。
ATTACH PARTITION FROM
ALTER TABLE table2 ATTACH PARTITION partition_expr FROM table1
此查询将数据分区从 复制table1
到table2
。请注意,既不会从table1
也不会从中删除数据table2
。要使查询成功运行,必须满足以下条件:
- 两个表必须具有相同的结构。
- 两个表必须具有相同的分区键。
REPLACE PARTITION
ALTER TABLE table2 REPLACE PARTITION partition_expr FROM table1
此查询将数据分区从 复制table1
到table2
并替换 中的现有分区table2
。请注意,数据不会从table1
,要使查询成功运行,必须满足以下条件:
- 两个表必须具有相同的结构。
- 两个表必须具有相同的分区键。
MOVE PARTITION TO TABLE
ALTER TABLE table_source MOVE PARTITION partition_expr TO TABLE table_dest
此查询将数据分区从 移动table_source
到,并从 中table_dest
删除数据table_source
,要使查询成功运行,必须满足以下条件:
- 两个表必须具有相同的结构。
- 两个表必须具有相同的分区键。
- 两个表必须是相同的引擎系列(复制或非复制)。
- 两个表必须具有相同的存储策略。
CLEAR COLUMN IN PARTITION
ALTER TABLE table_name CLEAR COLUMN column_name IN PARTITION partition_expr
重置分区中指定列中的所有值。如果DEFAULT
在创建表时确定了该子句,则此查询将列值设置为指定的默认值。
FREEZE PARTITION
此查询创建指定分区的本地备份。如果PARTITION
省略该子句,则查询会立即创建所有分区的备份。
ALTER TABLE table_name FREEZE [PARTITION partition_expr] [WITH NAME 'backup_name']
UNFREEZE PARTITION
从磁盘中删除freezed
具有指定名称的分区。如果PARTITION
省略该子句,则查询会立即删除所有分区的备份。
ALTER TABLE 'table_name' UNFREEZE [PARTITION 'part_expr'] WITH NAME 'backup_name'
CLEAR INDEX IN PARTITION
该查询的工作方式类似于CLEAR COLUMN
,但它重置索引而不是列数据。
ALTER TABLE table_name CLEAR INDEX index_name IN PARTITION partition_expr
FETCH PARTITION|PART
ALTER TABLE table_name FETCH PARTITION|PART partition_expr FROM 'path-in-zookeeper'
从另一台服务器下载分区,此查询仅适用于复制的表。查询执行以下操作:
- 从指定的分片下载分区|数据块。在“path-in-zookeeper”中,必须指定 ZooKeeper 中分片的路径。
- 然后查询把下载的数据放到表的
detached
目录下table_name
。使用ATTACH PARTITION|PART查询将数据添加到表中。
获取分区
ALTER TABLE users FETCH PARTITION 201902 FROM '/clickhouse/tables/01-01/visits';
ALTER TABLE users ATTACH PARTITION 201902;
获取数据块
ALTER TABLE users FETCH PART 201901_2_2_0 FROM '/clickhouse/tables/01-01/visits';
ALTER TABLE users ATTACH PART 201901_2_2_0;
注意:
- 查询不会被
ALTER ... FETCH PARTITION|PART
复制。它仅将部件或分区detached
放置到本地服务器上的目录中。 - 查询被
ALTER TABLE ... ATTACH
复制。它将数据添加到所有副本。数据从detached
目录添加到其中一个副本,并从相邻副本添加到其他副本。
在下载之前,系统会检查分区是否存在以及表结构是否匹配。从健康的副本中自动选择最合适的副本。虽然调用了查询ALTER TABLE
,但它不会更改表结构,也不会立即更改表中可用的数据。
MOVE PARTITION|PART
将分区或数据块移动到另一个卷或磁盘以用于MergeTree
-engine 表。
ALTER TABLE table_name MOVE PARTITION|PART partition_expr TO DISK|VOLUME 'disk_name'
ALTER TABLE t MOVE
查询:
- 不复制,因为不同的副本可以有不同的存储策略。
- 如果未配置指定的磁盘或卷,则返回错误。如果存储策略中指定的数据移动条件无法应用,查询也会返回错误。
- 在要移动的数据已被后台进程、并发
ALTER TABLE t MOVE
查询或后台数据合并的结果移动的情况下,可以返回错误。在这种情况下,用户不应执行任何其他操作。
UPDATE IN PARTITION
操作与指定过滤表达式匹配的指定分区中的数据。
ALTER TABLE [db.]table UPDATE column1 = expr1 [, ...] [IN PARTITION partition_id] WHERE filter_expr
DELETE IN PARTITION
删除与指定过滤表达式匹配的指定分区中的数据。
ALTER TABLE [db.]table DELETE [IN PARTITION partition_id] WHERE filter_expr
参考
- https://www.cnblogs.com/traditional/p/15218743.html
- https://mp.weixin.qq.com/s/VTTYMdY5A2SZNQdkZoXuhw
- https://blog.csdn.net/Night_ZW/article/details/112845382
- https://www.cnblogs.com/MrYang-11-GetKnow/p/16016400.html
- https://cloud.tencent.com/developer/article/1987040
- https://mp.weixin.qq.com/s/VTTYMdY5A2SZNQdkZoXuhw