数据分区和分桶

最近更新时间:2023-11-15 18:13:12

我的收藏
为了能高效处理大数据量的存储和计算,Doris 按分治思想对数据进行分割处理。
Doris 支持两层的数据划分。第一层是 Partition(分区),支持 Range (按范围)和 List(按枚举值) 的划分方式。第二层是 Bucket(分桶),仅支持 Hash 的划分方式。分区和分桶都是对数据进行横向分割。
也可以仅使用一层分区。使用一层分区时,只支持 Bucket 划分。下面我们来分别介绍下分区以及分桶。

分区(Partition)

分区用于将数据划分成不同区间, 逻辑上可以理解为将原始表划分成了多个子表。可以方便的按分区对数据进行管理,例如,删除数据时更加迅速。
Partition 列可以指定一列或多列,分区列必须为 KEY 列。多列分区的使用方式在后面 多列分区 小结介绍。
不论分区列是什么类型,在写分区值时,都需要加双引号。
分区数量理论上没有上限。
当不使用 Partition 建表时,系统会自动生成一个和表名同名的,全值范围的 Partition。该 Partition 对用户不可见,并且不可删改。
创建分区时不可添加范围重叠的分区。

Range 分区

Partition 支持通过 VALUES LESS THAN (...) 仅指定上界,系统会将前一个分区的上界作为该分区的下界,生成一个左闭右开的区间。同时,也支持通过 VALUES [...) 指定上下界,生成一个左闭右开的区间。
通过 VALUES [...) 同时指定上下界比较容易理解。这里举例说明,当使用 VALUES LESS THAN (...) 语句进行分区的增删操作时,分区范围的变化情况。
-- Range Partition

CREATE TABLE IF NOT EXISTS example_db.expamle_range_tbl
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`date` DATE NOT NULL COMMENT "数据灌入日期时间",
`timestamp` DATETIME NOT NULL COMMENT "数据灌入的时间戳",
`city` VARCHAR(20) COMMENT "用户所在城市",
`age` SMALLINT COMMENT "用户年龄",
`sex` TINYINT COMMENT "用户性别",
`last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用户最后一次访问时间",
`cost` BIGINT SUM DEFAULT "0" COMMENT "用户总消费",
`max_dwell_time` INT MAX DEFAULT "0" COMMENT "用户最大停留时间",
`min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用户最小停留时间"
)
ENGINE=OLAP
AGGREGATE KEY(`user_id`, `date`, `timestamp`, `city`, `age`, `sex`)
PARTITION BY RANGE(`date`)
(
PARTITION `p201701` VALUES LESS THAN ("2017-02-01"),
PARTITION `p201702` VALUES LESS THAN ("2017-03-01"),
PARTITION `p201703` VALUES LESS THAN ("2017-04-01")
)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16
PROPERTIES
(
"replication_num" = "3"
);
如上 expamle_range_tbl 示例,当建表完成后,查看分区会发现自动生成了如下3个分区:
show partitions from expamle_range_tbl;
p201701: [MIN_VALUE, 2017-02-01)
p201702: [2017-02-01, 2017-03-01)
p201703: [2017-03-01, 2017-04-01)
当我们使用 sql 增加一个分区 p201705 VALUES LESS THAN ("2017-06-01"):
alter table expamle_range_tbl add partition p201705 VALUES LESS THAN ("2017-06-01");
show partitions from expamle_range_tbl;
分区结果如下:
p201701: [MIN_VALUE, 2017-02-01)
p201702: [2017-02-01, 2017-03-01)
p201703: [2017-03-01, 2017-04-01)
p201705: [2017-04-01, 2017-06-01)
此时我们删除分区 p201703:
alter table expamle_range_tbl drop partition p201703;
show partitions from expamle_range_tbl;
则分区结果如下:
p201701: [MIN_VALUE, 2017-02-01)
p201702: [2017-02-01, 2017-03-01)
p201705: [2017-04-01, 2017-06-01)
注意
注意到 p201702 和 p201705 的分区范围并没有发生变化,而这两个分区之间,出现了一个空洞:[2017-03-01, 2017-04-01)。即如果导入的数据范围在这个空洞范围内,是无法导入的。
继续删除分区 p201702:
alter table expamle_range_tbl drop partition p201702;
show partitions from expamle_range_tbl;
分区结果如下:
p201701: [MIN_VALUE, 2017-02-01)
p201705: [2017-04-01, 2017-06-01)
说明
空洞范围变为:[2017-02-01, 2017-04-01)。
现在增加一个分区 p201702new VALUES LESS THAN ("2017-03-01"):
alter table expamle_range_tbl add partition p201702new VALUES LESS THAN ("2017-03-01");
show partitions from expamle_range_tbl;
分区结果如下:
p201701: [MIN_VALUE, 2017-02-01)
p201702new: [2017-02-01, 2017-03-01)
p201705: [2017-04-01, 2017-06-01)
说明
可以看到空洞范围缩小为:[2017-03-01, 2017-04-01)。
现在删除分区 p201701,并添加分区 p201612 VALUES LESS THAN ("2017-01-01"):
alter table expamle_range_tbl drop partition p201701;
alter table expamle_range_tbl add partition p201612 VALUES LESS THAN ("2017-01-01");
show partitions from expamle_range_tbl;
分区结果如下:
p201612: [MIN_VALUE, 2017-01-01)
p201702new: [2017-02-01, 2017-03-01)
p201705: [2017-04-01, 2017-06-01)
说明
即出现了一个新的空洞:[2017-01-01, 2017-02-01)。
综上,分区的删除不会改变已存在分区的范围。删除分区可能出现空洞。通过 VALUES LESS THAN 语句增加分区时,分区的下界紧接上一个分区的上界。

多列分区

Range 分区除了上述我们看到的单列分区,也支持多列分区,示例如下:
-- Range Partition

CREATE TABLE IF NOT EXISTS example_db.expamle_range_multi_partiton_key_tbl
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`date` DATE NOT NULL COMMENT "数据灌入日期时间",
`timestamp` DATETIME NOT NULL COMMENT "数据灌入的时间戳",
`city` VARCHAR(20) COMMENT "用户所在城市",
`age` SMALLINT COMMENT "用户年龄",
`sex` TINYINT COMMENT "用户性别",
`last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用户最后一次访问时间",
`cost` BIGINT SUM DEFAULT "0" COMMENT "用户总消费",
`max_dwell_time` INT MAX DEFAULT "0" COMMENT "用户最大停留时间",
`min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用户最小停留时间"
)
ENGINE=OLAP
AGGREGATE KEY(`user_id`, `date`, `timestamp`, `city`, `age`, `sex`)
PARTITION BY RANGE(`date`, `user_id`)
(
PARTITION `p201701_1000` VALUES LESS THAN ("2017-02-01", "1000"),
PARTITION `p201702_2000` VALUES LESS THAN ("2017-03-01", "2000"),
PARTITION `p201703_all` VALUES LESS THAN ("2017-04-01")
)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16
PROPERTIES
(
"replication_num" = "3"
);
在以上示例中,我们指定 date(DATE 类型)和 user_id(INT 类型)作为分区列。以上示例最终得到的分区如下:
show partitions from expamle_range_multi_partiton_key_tbl;
* p201701_1000: [(MIN_VALUE, MIN_VALUE), ("2017-02-01", "1000") )
* p201702_2000: [("2017-02-01", "1000"), ("2017-03-01", "2000") )
* p201703_all: [("2017-03-01", "2000"), ("2017-04-01", MIN_VALUE))
注意
最后一个分区用户缺省只指定了 date 列的分区值,所以 user_id 列的分区值会默认填充 MIN_VALUE。当用户插入数据时,分区列值会按照顺序依次比较,最终得到对应的分区。
举例如下:
* 数据 --> 分区
* 2017-01-01, 200 --> p201701_1000
* 2017-01-01, 2000 --> p201701_1000
* 2017-02-01, 100 --> p201701_1000
* 2017-02-01, 2000 --> p201702_2000
* 2017-02-15, 5000 --> p201702_2000
* 2017-03-01, 2000 --> p201703_all
* 2017-03-10, 1 --> p201703_all
* 2017-04-01, 1000 --> 无法导入
* 2017-05-01, 1000 --> 无法导入
验证方法: 插入一条数据并检查存入到哪个分区。分区字段 VisibleVersionTime、VisibleVersion 刚刚有更新的分区即为刚插入数据所在的分区。
insert into expamle_range_multi_partiton_key_tbl values (200, '2017-01-01', '2017-01-01 12:00:05', 'Beijing', 25, 1, '2017-01-01 12:00:05', 100, 30, 10);
insert into expamle_range_multi_partiton_key_tbl values (2000, '2017-01-01', '2017-01-01 16:10:05', 'Shanghai', 33, 1, '2017-01-01 16:10:05', 800, 50, 1);
insert into expamle_range_multi_partiton_key_tbl values (200, '2017-02-01', '2017-01-01 16:10:05', 'Guangzhou', 22, 0, '2017-02-01 16:10:05', 80, 200, 1);
show partitions from expamle_range_multi_partiton_key_tbl\\G

List 分区

分区列支持 BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, LARGEINT, DATE, DATETIME, CHAR, VARCHAR 数据类型,分区值为枚举值。只有当数据为目标分区枚举值其中之一时,才可以命中分区。Partition 支持通过 VALUES IN (...) 来指定每个分区包含的枚举值。 下面通过示例说明,进行分区的增删操作时,分区的变化。
-- List Partition

CREATE TABLE IF NOT EXISTS example_db.expamle_list_tbl
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`date` DATE NOT NULL COMMENT "数据灌入日期时间",
`timestamp` DATETIME NOT NULL COMMENT "数据灌入的时间戳",
`city` VARCHAR(20) NOT NULL COMMENT "用户所在城市",
`age` SMALLINT COMMENT "用户年龄",
`sex` TINYINT COMMENT "用户性别",
`last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用户最后一次访问时间",
`cost` BIGINT SUM DEFAULT "0" COMMENT "用户总消费",
`max_dwell_time` INT MAX DEFAULT "0" COMMENT "用户最大停留时间",
`min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用户最小停留时间"
)
ENGINE=olap
AGGREGATE KEY(`user_id`, `date`, `timestamp`, `city`, `age`, `sex`)
PARTITION BY LIST(`city`)
(
PARTITION `p_cn` VALUES IN ("Beijing", "Shanghai", "Hong Kong"),
PARTITION `p_usa` VALUES IN ("New York", "San Francisco"),
PARTITION `p_jp` VALUES IN ("Tokyo")
)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16
PROPERTIES
(
"replication_num" = "3"
);
如上 example_list_tbl 示例,当建表完成后,会自动生成如下3个分区:
show partitions from expamle_list_tbl;
p_cn: ("Beijing", "Shanghai", "Hong Kong")
p_usa: ("New York", "San Francisco")
p_jp: ("Tokyo")
当我们增加一个分区 p_uk VALUES IN ("London")
alter table expamle_list_tbl add partition p_uk VALUES IN ("London");
show partitions from expamle_list_tbl;
分区结果如下:
p_cn: ("Beijing", "Shanghai", "Hong Kong")
p_usa: ("New York", "San Francisco")
p_jp: ("Tokyo")
p_uk: ("London")
当我们删除分区 p_jp,分区结果如下:
alter table expamle_list_tbl drop partition p_jp;
show partitions from expamle_list_tbl;
p_cn: ("Beijing", "Shanghai", "Hong Kong")
p_usa: ("New York", "San Francisco")
p_uk: ("London")

多列分区

List 分区也支持多列分区,示例如下:
-- List Partition

CREATE TABLE IF NOT EXISTS example_db.expamle_list_multi_partiton_key_tbl
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`date` DATE NOT NULL COMMENT "数据灌入日期时间",
`timestamp` DATETIME NOT NULL COMMENT "数据灌入的时间戳",
`city` VARCHAR(20) NOT NULL COMMENT "用户所在城市",
`age` SMALLINT COMMENT "用户年龄",
`sex` TINYINT COMMENT "用户性别",
`last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用户最后一次访问时间",
`cost` BIGINT SUM DEFAULT "0" COMMENT "用户总消费",
`max_dwell_time` INT MAX DEFAULT "0" COMMENT "用户最大停留时间",
`min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用户最小停留时间"
)
ENGINE=olap
AGGREGATE KEY(`user_id`, `date`, `timestamp`, `city`, `age`, `sex`)
PARTITION BY LIST(`user_id`, `city`)
(
PARTITION `p1_city` VALUES IN (("1", "Beijing"), ("1", "Shanghai")),
PARTITION `p2_city` VALUES IN (("2", "Beijing"), ("2", "Shanghai")),
PARTITION `p3_city` VALUES IN (("3", "Beijing"), ("3", "Shanghai"))
)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16
PROPERTIES
(
"replication_num" = "3"
);
在以上示例中,我们指定 user_id(INT 类型) 和 city(VARCHAR 类型) 作为分区列。以上示例最终得到的分区如下:
show partitions from expamle_list_multi_partiton_key_tbl;
* p1_city: [("1", "Beijing"), ("1", "Shanghai")]
* p2_city: [("2", "Beijing"), ("2", "Shanghai")]
* p3_city: [("3", "Beijing"), ("3", "Shanghai")]
当用户插入数据时,分区列值会按照顺序依次比较,最终得到对应的分区。举例如下:
* 数据 ---> 分区
* 1, Beijing ---> p1_city
* 1, Shanghai ---> p1_city
* 2, Shanghai ---> p2_city
* 3, Beijing ---> p3_city
* 1, Tianjin ---> 无法导入
* 4, Beijing ---> 无法导入
验证方法: 插入一条数据并检查存入到哪个分区。分区字段 VisibleVersionTime、VisibleVersion 刚刚有更新的分区即为刚插入数据所在的分区。
insert into expamle_list_multi_partiton_key_tbl values (1, '2017-01-01', '2017-01-01 12:00:05', 'Beijing', 25, 1, '2017-01-01 12:00:05', 100, 30, 10);
show partitions from expamle_list_multi_partiton_key_tbl\\G

分桶(Bucket)

根据分桶列的 hash 值将数据划分成不同的 Bucket。
如果使用了 Partition,则 DISTRIBUTED ... 语句描述的是数据在各个分区内的划分规则。如果不使用 Partition,则描述的是对整个表的数据的划分规则。
分桶列可以选择多列,但必须为 Key 列。分桶列可以和 Partition 列相同或不同。
分桶列的选择,是在 查询吞吐查询并发 之间的一种权衡:
1.1 如果选择多个分桶列,则数据分布更均匀。如果一个查询条件不包含所有分桶列的等值条件,那么该查询会触发所有分桶同时扫描,这样查询的吞吐会增加,单个查询的延迟随之降低。这个方式适合大吞吐低并发的查询场景。
1.2 如果仅选择一个或少数分桶列,则对应的点查询可以仅触发一个分桶扫描。此时,当多个点查询并发时,这些查询有较大的概率分别触发不同的分桶扫描,各个查询之间的 IO 影响较小(尤其当不同桶分布在不同磁盘上时),所以这种方式适合高并发的点查询场景。
分桶的数量理论上没有上限。

最佳实践

关于分桶列的选择

建议采用区分度大的列做分桶, 避免出现数据倾斜。
为方便数据恢复, 建议单个 Bucket 的 size 不要太大, 保持在10GB以内, 所以建表或增加 Partition 时请合理考虑 Bucket 数目, 其中不同 Partition 可指定不同的 Bucket 数。

关于 Partition 和 Bucket 的数量和数据量的建议

一个表的 Tablet 总数量等于(Partition num * Bucket num)。
一个表的 Tablet 数量,在不考虑扩容的情况下,推荐略多于整个集群的磁盘数量。
单个 Tablet 的数据量理论上没有上下界,但建议在 1G - 10G 的范围内。如果单个 Tablet 数据量过小,则数据的聚合效果不佳,且元数据管理压力大。如果数据量过大,则不利于副本的迁移、补齐,且会增加 Schema Change 或者 Rollup 操作失败重试的代价(这些操作失败重试的粒度是 Tablet)。
当 Tablet 的数据量原则和数量原则冲突时,建议优先考虑数据量原则。
在建表时,每个分区的 Bucket 数量统一指定。但是在动态增加分区时(ADD PARTITION),可以单独指定新分区的 Bucket 数量。可以利用这个功能方便的应对数据缩小或膨胀。
一个 Partition 的 Bucket 数量一旦指定,不可更改。所以在确定 Bucket 数量时,需要预先考虑集群扩容的情况。例如当前只有 3 台 host,每台 host 有 1 块盘。如果 Bucket 的数量只设置为 3 或更小,那么后期即使再增加机器,也不能提高并发度。
举一些例子:假设在有10台 BE,每台BE一块磁盘的情况下。如果一个表总大小为 500MB,则可以考虑4-8个分片。5GB:8-16个分片。50GB:32个分片。500GB:建议分区,每个分区大小在 50GB 左右,每个分区16-32个分片。5TB:建议分区,每个分区大小在 50GB 左右,每个分区16-32个分片。
说明
表的数据量可以通过 SHOW DATA 命令查看,结果除以副本数,即表的数据量。

复合分区与单分区

复合分区

第一级称为 Partition,即分区。用户可以指定某一维度列作为分区列(当前只支持整型和时间类型的列),并指定每个分区的取值范围。
第二级称为 Distribution,即分桶。用户可以指定一个或多个维度列以及桶数对数据进行 HASH 分布。
以下场景推荐使用复合分区:
有时间维度或类似带有有序值的维度,可以以这类维度列作为分区列。分区粒度可以根据导入频次、分区数据量等进行评估。
历史数据删除需求:如有删除历史数据的需求(例如仅保留最近 N 天的数据)。使用复合分区,可以通过删除历史分区来达到目的。也可以通过在指定分区内发送 DELETE 语句进行数据删除。
解决数据倾斜问题:每个分区可以单独指定分桶数量。如按天分区,当每天的数据量差异很大时,可以通过指定分区的分桶数,合理划分不同分区的数据,分桶列建议选择区分度大的列。

单分区

用户也可以不使用复合分区(不选择分区列),即使用单分区。则数据只做 HASH 分布。

常见问题

Failed to create partition [xxx] . Timeout

Doris 建表是按照 Partition 粒度依次创建的。当一个 Partition 创建失败时,可能会报这个错误。即使不使用 Partition,当建表出现问题时,也会报 Failed to create partition,因为如前文所述,Doris 会为没有指定 Partition 的表创建一个不可更改的默认的 Partition。 当遇到这个错误时,通常是 BE 在创建数据分片时遇到了问题。可以参照以下步骤排查:
1. 在 fe.log 中,查找对应时间点的 Failed to create partition 日志。在该日志中,会出现一系列类似 {10001-10010} 字样的数字对。数字对的第一个数字表示 Backend ID,第二个数字表示 Tablet ID。如上这个数字对,表示 ID 为 10001 的 Backend 上,创建 ID 为 10010 的 Tablet 失败了。
2. 前往对应 Backend 的 be.INFO 日志,查找对应时间段内,tablet id 相关的日志,可以找到错误信息。
3. 以下罗列一些常见的 tablet 创建失败错误,包括但不限于:
4. BE 没有收到相关 task,此时无法在 be.INFO 中找到 tablet id 相关日志或者 BE 创建成功,但汇报失败。以上问题,请检查 FE 和 BE 的连通性。
5. 预分配内存失败。可能是表中一行的字节长度超过了 100KB。
6. Too many open files。打开的文件句柄数超过了 Linux 系统限制。需修改 Linux 系统的句柄数限制。
如果创建数据分片时超时,也可以通过在 fe.conf 中设置 tablet_create_timeout_second=xxx 以及 max_create_table_timeout_second=xxx 来延长超时时间。其中 tablet_create_timeout_second 默认是1秒,max_create_table_timeout_second 默认是60秒,总体的超时时间为 min(tablet_create_timeout_second * replication_num, max_create_table_timeout_second),具体参数设置参见 FE 配置项

分区数量和分桶数量是否有上限?

分区数量和分表数量没有上限,但过多的分区和分桶数量可能会对doris性能造成影响。
为避免一次创建过多分区,FE上`max_dynamic_partition_num`和`max_multi_partition_num`参数,分别对**自动分区创建分区**和**批量创建分区**的数量进行了限制。

分区的分桶数量和数据副本数量如何设置?

手工创建分区时,可以指定创建分区的副本数量和分桶数量。如果没有指定,则使用表的`replication_allocation`和`buckets`参数。
自动创建分区时,创建的分区的副本数量和分桶数量使用表的`dynamic_partition.replication_allocation`和`dynamic_partition.buckets`参数。

分桶数量是否能修改?

已创建的分区的分桶数量不可修改。
对于使用Range分区的表,可以修改表的bucket数量。对于修改之后手工创建的表的分区,将使用修改后的bucket分桶数量,对于已创建的分区不生效。如果开启了自动分区,这个修改不会影响自动分区新创建的分区的bucket分桶数量,如需要修改自动分区分桶数量,需要修改`dynamic_partition.buckets`,修改后新自动分区创建的bucket分桶数量创建分区,对于已创建的分区不生效。
如需修改已有表的分区分桶数量。(**操作过程涉及数据,注意做好数据备份,关注doris系统负载**)
对于未使用Range分区的表,只能通过新建表的方式修改分桶数量,再将数据导入新表的方式进行修改。
对于已有分区,可以通过创建临时分区,将对应分区导入临时分区,之后再将临时分区设置为已有分区名称。

分桶 key 是否能修改?

不可以,分桶key只能在建表时设置,后续不能更改。

是否可以在已有表上开启自动分桶功能?

不可以,自动分桶功能只能在创建表时开启。

替换分区后原有分区是否存在?

分区替换成功后,被替换的分区将被删除且不可恢复。