前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、数据分布

Doris 中,数据分布通过合理的分区和分桶策略,将数据高效地映射到各个数据分片(Tablet上,从而充分利用多节点的存储和计算能力,支持大规模数据的高效存储和查询。

1、数据分布概览

1)数据写入

数据写入时,Doris 首先根据表的分区策略将数据行分配到对应的分区。接着,根据分桶策略将数据行进一步映射到分区内的具体分片,从而确定了数据行的存储位置。

2)查询执行

查询运行时,Doris 的优化器会根据分区和分桶策略裁剪数据,最大化减少扫描范围。在涉及 JOIN 或聚合查询时,可能会发生跨节点的数据传输(Shuffle)。合理的分区和分桶设计可以减少 Shuffle 并充分利用 Colocate Join 优化查询性能。

2、节点与存储架构

1)节点类型

Doris 集群由以下两种节点组成:

  • FE 节点(Frontend:管理集群元数据(如表、分片),负责 SQL 的解析与执行规划。
  • BE 节点(Backend:存储数据,负责计算任务的执行。BE 的结果汇总后返回至 FE,再返回给用户。

2)数据分片(Tablet)

BE 节点的存储数据分片的数据,每个分片是 Doris 中数据管理的最小单元,也是数据移动和复制的基本单位。

3、分区、分桶

1)分区策略

分区是数据组织的第一层逻辑划分,用于将表中的数据划分为更小的子集。Doris 提供以下两种分区类型和三种分区模式

a、分区类型

  • Range 分区:根据分区列的值范围将数据行分配到对应分区。
  • List 分区:根据分区列的具体值将数据行分配到对应分区。

b、分区模式

  • 手动分区:用户手动创建分区(如建表时指定或通过 ALTER 语句增加)。
  • 动态分区:系统根据时间调度规则自动创建分区,但写入数据时不会按需创建分区。
  • 自动分区:数据写入时,系统根据需要自动创建相应的分区,使用时注意脏数据生成过多的分区。

2)分桶策略

分桶是数据组织的第二层逻辑划分,用于在分区内将数据行进一步划分到更小的单元。Doris 支持以下两种分桶方式:

  • Hash 分桶:通过计算分桶列值的 crc32 哈希值,并对分桶数取模,将数据行均匀分布到分片中。
  • Random 分桶:随机分配数据行到分片中。使用 Random 分桶时,可以使用 load_to_single_tablet 优化小规模数据的快速写入。

4、数据分布

1)数据分布优化

a、Colocate Join

对于需要频繁进行 JOIN 或聚合查询的大表,可以启用 Colocate 策略,将相同分桶列值的数据放置在同一物理节点上,减少跨节点的数据传输,从而显著提升查询性能。

b、分区裁剪

查询时,Doris 可以通过过滤条件裁剪掉不相关的分区,从而减少数据扫描范围,降低 I/O 开销。

c、分桶并行

查询时,合理的分桶数可以充分利用机器的计算资源和 I/O 资源。

2)数据分布目标

  1. 均匀数据分布 确保数据均匀分布在各 BE 节点上,避免数据倾斜导致部分节点过载,从而提高系统整体性能。
  2. 优化查询性能 合理的分区裁剪可以大幅减少扫描的数据量,合理的分桶数可以提升计算并行度,合理利用 Colocate 可以降低 Shuffle 成本,提升 JOIN 和聚合查询效率。
  3. 灵活数据管理
    • 按时间分区保存冷数据(HDD)与热数据(SSD)。
    • 定期删除历史分区释放存储空间。
  4. 控制元数据规模 每个分片的元数据存储在 FEBE 中,因此需要合理控制分片数量。经验值建议:
    • 1000 万分片,FE 至少需 100G 内存。
    • 单个 BE 承载的分片数应小于 2 万。
  5. 优化写入吞吐
    • 分桶数应合理控制(建议 < 128),以避免写入性能下降。
    • 每次写入的分区数量应适量(建议每次写入少量分区)。

二、分区

分区方式 区别 场景
手动分区 需要管理员手动创建、删除和管理分区,灵活性高,但维护成本也高。例如,通过ALTER TABLE语句添加或删除分区4。 适用于数据分布规律明确、业务需求特殊且对分区管理有精细控制要求的场景,如特定业务规则下的少量关键数据表,需要根据业务逻辑精确划分和管理分区。
自动分区 在数据导入过程中,系统根据预定义的规则自动创建分区,支持按时间维度的 Range 分区和支持多种数据类型的 List 分区。无需预先创建分区,减少了人工维护工作量2。 适合处理历史数据重放计算,以及分区列包含大量离散值、数据分布零散或难以预测的场景,如电商订单数据按地区或商品类别进行分区,系统可根据实际数据自动创建分区2。
动态分区 主要支持按照时间维度的 Range 分区,以建表时的现实时间为标准,根据设置的分区单位、历史分区数量和未来分区数量,自动创建和回收数据分区,实现数据的滚动更新。例如,按天为单位创建分区,设置start为 - 7,end3,可预创建未来 3 天的数据分区,并自动回收距今超过 7 天的历史数据分区 2 适用于实时数据收集场景,如 ODS 层直接从外部数据源(如 Kafka)接收数据,以及只保留近期 n 天数据的场景,如日志数据、时序数据的管理,可自动控制数据的生命周期,减少存储压力23

1、手动分区

  • 分区列可以指定一列或多列,分区列必须为 KEY 列。
  • 创建分区时不可添加范围重叠的分区。
  • 不论分区列是什么类型,在写分区值时,都需要加双引号。
  • 当不使用分区建表时,系统会自动生成一个和表名同名的,全值范围的分区。该分区对用户不可见,并且不可删改。
  • 分区数量理论上没有上限。但默认限制每张表 4096 个分区,如果想突破这个限制,可以修改 FE 配置 max_multi_partition_nummax_dynamic_partition_num

1)Range 分区

分区列通常为时间列,以方便的管理新旧数据。Range 分区支持的列类型 DATE, DATETIME,TINYINT, SMALLINT, INT,BIGINT, LARGEINT分区信息,支持四种写法:

a、FIXED RANGE

定义分区的左闭右开区间。

PARTITION BY RANGE(`date`)
(
    PARTITION `p201701` VALUES [("2017-01-01"),  ("2017-02-01")),
    PARTITION `p201702` VALUES [("2017-02-01"), ("2017-03-01")),
    PARTITION `p201703` VALUES [("2017-03-01"), ("2017-04-01"))
)

b、LESS THAN

仅定义分区上界。下界由上一个分区的上界决定。

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"),
    PARTITION `p2018` VALUES [("2018-01-01"), ("2019-01-01")),
    PARTITION `other` VALUES LESS THAN (MAXVALUE)
)

c、BATCH RANGE

批量创建数字类型和时间类型的 RANGE 分区,定义分区的左闭右开区间,设定步长。

PARTITION BY RANGE(age)(
    FROM (1) TO (100) INTERVAL 10
)

PARTITION BY RANGE(`date`)(
    FROM ("2000-11-14") TO ("2021-11-14") INTERVAL 2 YEAR
)

d、MULTI RANGE

批量创建 RANGE 分区,定义分区的左闭右开区间。示例如下:

PARTITION BY RANGE(col)                                                                                
(                                                                                                   
   FROM ("2000-11-14") TO ("2021-11-14") INTERVAL 1 YEAR,                                   
   FROM ("2021-11-14") TO ("2022-11-14") INTERVAL 1 MONTH,                                                   
   FROM ("2022-11-14") TO ("2023-01-03") INTERVAL 1 WEEK,                                                     
   FROM ("2023-01-03") TO ("2023-01-14") INTERVAL 1 DAY,
   PARTITION p_20230114 VALUES [('2023-01-14'), ('2023-01-15'))                                               
) 

2)List 分区

分区列支持 BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, LARGEINT,DATE, DATETIME, CHAR, VARCHAR` 数据类型,分区值为枚举值。只有当数据为目标分区枚举值其中之一时,才可以命中分区。

PARTITION BY LIST(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"))
)

3)NULL 分区

PARTITION 列默认必须为 NOT NULL 列,如果需要使用 NULL 列,应设置 session variable allow_partition_column_nullable = true

  • LIST PARTITION,支持真正的 NULL 分区。
  • RANGE PARTITIONNULL 值会被划归 最小的 LESS THAN 分区。分列如下:

a、LIST 分区

mysql> create table null_list(
    -> k0 varchar null
    -> )
    -> partition by list (k0)
    -> (
    -> PARTITION pX values in ((NULL))
    -> )
    -> DISTRIBUTED BY HASH(`k0`) BUCKETS 1
    -> properties("replication_num" = "1");
Query OK, 0 rows affected (0.11 sec)

mysql> insert into null_list values (null);
Query OK, 1 row affected (0.19 sec)

mysql> select * from null_list;
+------+
| k0   |
+------+
| NULL |
+------+
1 row in set (0.18 sec)

b、RANGE 分区- LESS THAN

mysql> create table null_range(
    -> k0 int null
    -> )
    -> partition by range (k0)
    -> (
    -> PARTITION p10 values less than (10),
    -> PARTITION p100 values less than (100),
    -> PARTITION pMAX values less than (maxvalue)
    -> )
    -> DISTRIBUTED BY HASH(`k0`) BUCKETS 1
    -> properties("replication_num" = "1");
Query OK, 0 rows affected (0.12 sec)

mysql> insert into null_range values (null);
Query OK, 1 row affected (0.19 sec)

mysql> select * from null_range partition(p10);
+------+
| k0   |
+------+
| NULL |
+------+
1 row in set (0.18 sec)

2、动态分区

动态分区会按照设定的规则,滚动添加、删除分区,从而实现对表分区的生命周期管理(TTL),减少数据存储压力。在日志管理,时序数据管理等场景,通常可以使用动态分区能力滚动删除过期的数据。

下图中展示了使用动态分区进行生命周期管理,其中指定了以下规则:

  • 动态分区调度单位 dynamic_partition.time_unitDAY,按天组织分区;
  • 动态分区起始偏移量 dynamic_partition.start 设置为 -1,保留一天前分区;
  • 动态分区结束偏移量 dynamic_partition.end 设置为 2,保留未来两天分区

依据以上规则,随着时间推移,总会保留 4 个分区,即过去一天分区,当天分区与未来两天分区:

image-20250623154306174

1)使用限制

在使用动态分区时,需要遵守以下规则:

  • 动态分区与跨集群复制(CCR)同时使用时会失效;
  • 动态分区只支持在 DATE / DATETIME 列上进行 Range 类型的分区;
  • 动态分区只支持单一分区键。

2)创建动态分区

在建表时,通过指定 dynamic_partition 属性,可以创建动态分区表。

CREATE TABLE tbl1 (
    order_id    BIGINT,
    create_dt   DATE,
    username    VARCHAR(20)
)
PARTITION BY RANGE(create_dt) ()
DISTRIBUTED BY HASH(create_dt)
PROPERTIES (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-7",
    "dynamic_partition.end" = "3",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.buckets" = "32"
);

3)管理动态分区

a、修改动态分区属性

在使用语句修改动态分区时,不会立即生效。会以 dynamic_partition_check_interval_seconds 参数指定的时间间隔轮训检查 dynamic partition 分区,完成需要的分区创建与删除操作。

下例中通过 ALTER TABLE 语句,将非动态分区表修改为动态分区:

CREATE TABLE test_dynamic_partition(
    order_id    BIGINT,
    create_dt   DATE,
    username    VARCHAR(20)
)
DUPLICATE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 10;




ALTER TABLE test_partition SET (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-1",
    "dynamic_partition.end" = "2",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.create_history_partition" = "true"
);

b、查看动态分区调度情况

通过 SHOW-DYNAMIC-PARTITION 可以查看当前数据库下,所有动态分区表的调度情况:

SHOW DYNAMIC PARTITION TABLES;
+-----------+--------+----------+-------------+------+--------+---------+-----------+----------------+---------------------+--------+------------------------+----------------------+-------------------------+
| TableName | Enable | TimeUnit | Start       | End  | Prefix | Buckets | StartOf   | LastUpdateTime | LastSchedulerTime   | State  | LastCreatePartitionMsg | LastDropPartitionMsg | ReservedHistoryPeriods  |
+-----------+--------+----------+-------------+------+--------+---------+-----------+----------------+---------------------+--------+------------------------+----------------------+-------------------------+
| d3        | true   | WEEK     | -3          | 3    | p      | 1       | MONDAY    | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | [2021-12-01,2021-12-31] |
| d5        | true   | DAY      | -7          | 3    | p      | 32      | N/A       | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | NULL                    |
| d4        | true   | WEEK     | -3          | 3    | p      | 1       | WEDNESDAY | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | NULL                    | 
| d6        | true   | MONTH    | -2147483648 | 2    | p      | 8       | 3rd       | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | NULL                    |
| d2        | true   | DAY      | -3          | 3    | p      | 32      | N/A       | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | NULL                    |
| d7        | true   | MONTH    | -2147483648 | 5    | p      | 8       | 24th      | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | NULL                    |
+-----------+--------+----------+-------------+------+--------+---------+-----------+----------------+---------------------+--------+------------------------+----------------------+-------------------------+
7 rows in set (0.02 sec)

c、历史分区管理

在使用 startend 属性指定动态分区数量时,为了避免一次性创建所有的分区造成等待时间过长,不会创建历史分区,只会创建当前时间以后得分区。如果需要一次性创建所有分区,需要开启 create_history_partition 参数。

例如当前日期为 2024-10-11,指定 start = -2,end = 2:

  • 如果指定了 create_history_partition = true,立即创建所有分区,即 [10-09, 10-13] 五个分区;
  • 如果指定了 create_history_partition = false,只创建包含 10-11 以后的分区,即 [10-11, 10-13] 三个分区。

4)动态分区最佳实践

示例 1:按天分区,只保留过去 7 天的及当天分区,并且预先创建未来 3 天的分区。

CREATE TABLE tbl1 (
    order_id    BIGINT,
    create_dt   DATE,
    username    VARCHAR(20)
)
PARTITION BY RANGE(create_dt) ()
DISTRIBUTED BY HASH(create_dt)
PROPERTIES (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-7",
    "dynamic_partition.end" = "3",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.buckets" = "32"
);

示例 2:按月分区,不删除历史分区,并且预先创建未来 2 个月的分区。同时设定以每月 3 号为起始日。

CREATE TABLE tbl1 (
    order_id    BIGINT,
    create_dt   DATE,
    username    VARCHAR(20)
)
PARTITION BY RANGE(create_dt) ()
DISTRIBUTED BY HASH(create_dt)
PROPERTIES (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "MONTH",
    "dynamic_partition.end" = "2",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.buckets" = "8",
    "dynamic_partition.start_day_of_month" = "3"
);

示例 3:按天分区,保留过去 10 天及未来 10 天分区,并且保留 [2020-06-01,2020-06-20] 及 [2020-10-31,2020-11-15] 期间的历史数据。

CREATE TABLE tbl1 (
    order_id    BIGINT,
    create_dt   DATE,
    username    VARCHAR(20)
)
PARTITION BY RANGE(create_dt) ()
DISTRIBUTED BY HASH(create_dt)
PROPERTIES (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-10",
    "dynamic_partition.end" = "10",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.buckets" = "8",
    "dynamic_partition.reserved_history_periods"="[2020-06-01,2020-06-20],[2020-10-31,2020-11-15]"
);

5)参数配置

a、动态分区属性参数

动态分区的规则参数以 dynamic_partition 为前缀,可以设置以下规则参数:

参数 必选 说明
dynamic_partition.enable 是否开启动态分区特性。可以指定为 TRUE 或 FALSE。如果指定了动态分区其他必填参数,默认为 TRUE。
dynamic_partition.time_unit 动态分区调度的单位。可指定为 HOURDAYWEEKMONTHYEAR。分别表示按小时、按天、按星期、按月、按年进行分区创建或删除:
dynamic_partition.start 动态分区的起始偏移,为负数。默认值为 -2147483648,即不删除历史分区。根据 time_unit 属性的不同,以当天(星期/月)为基准,分区范围在此偏移之前的分区将会被删除。此偏移之后至当前时间的历史分区如不存在,是否创建取决于 dynamic_partition.create_history_partition
dynamic_partition.end 动态分区的结束偏移,为正数。根据 time_unit 属性的不同,以当天(星期/月)为基准,提前创建对应范围的分区。
dynamic_partition.prefix 动态创建的分区名前缀。
dynamic_partition.buckets 动态创建的分区所对应的分桶数。设置该参数后会覆盖 DISTRIBUTED 中指定的分桶数。量。
dynamic_partition.replication_num 动态创建的分区所对应的副本数量,如果不填写,则默认为该表创建时指定的副本数量。
dynamic_partition.create_history_partition 默认为 false。当置为 true 时,Doris 会自动创建所有分区,具体创建规则见下文。同时,FE 的参数 max_dynamic_partition_num 会限制总分区数量,以避免一次性创建过多分区。当期望创建的分区个数大于 max_dynamic_partition_num 值时,操作将被禁止。当不指定 start 属性时,该参数不生效。
dynamic_partition.history_partition_num create_history_partitiontrue 时,该参数用于指定创建历史分区数量。默认值为 -1,即未设置。该变量与 dynamic_partition.start 作用相同,建议同时只设置一个。
dynamic_partition.start_day_of_week time_unitWEEK 时,该参数用于指定每周的起始点。取值为 1 到 7。其中 1 表示周一,7 表示周日。默认为 1,即表示每周以周一为起始点。
dynamic_partition.start_day_of_month time_unitMONTH 时,该参数用于指定每月的起始日期。取值为 1 到 28。其中 1 表示每月 1 号,28 表示每月 28 号。默认为 1,即表示每月以 1 号为起始点。暂不支持以 29、30、31 号为起始日,以避免因闰年或闰月带来的歧义。
dynamic_partition.reserved_history_periods 需要保留的历史分区的时间范围。当dynamic_partition.time_unit 设置为 “DAY/WEEK/MONTH/YEAR” 时,需要以 [yyyy-MM-dd,yyyy-MM-dd],[...,...] 格式进行设置。当dynamic_partition.time_unit 设置为 “HOUR” 时,需要以 [yyyy-MM-dd HH:mm:ss,yyyy-MM-dd HH:mm:ss],[...,...] 的格式来进行设置。如果不设置,默认为 "NULL"
dynamic_partition.time_zone 动态分区时区,默认为当前服务器的系统时区,如 Asia/Shanghai。更多时区设置可以参考时区管理

b、FE 配置参数

可以在 FE 配置文件或通过 ADMIN SET FRONTEND CONFIG 命令修改 FE 中的动态分区参数配置:

参数 默认值 说明
dynamic_partition_enable false 是否开启 Doris 的动态分区功能。该参数只影响动态分区表的分区操作,不影响普通表。
dynamic_partition_check_interval_seconds 600 动态分区线程的执行频率,单位为秒。
max_dynamic_partition_num 500 用于限制创建动态分区表时可以创建的最大分区数,避免一次创建过多分区。

3、自动分区

自动分区功能通过数据驱动的动态分区生成机制,填补了传统手动分区与时间驱动动态分区的应用空白,尤其适用于分区列离散且不可预测的场景,既减少了人工维护成本,又提升了大数据场景下的表结构管理效率。

数据分布零散或不可预测时的分区管理难题:当分区列(如时间、地域 ID 等)的数据分布难以提前规划,或手动创建分区成本过高(如分区数量庞大)时,自动分区功能可避免建表或改表时的繁琐操作。

以时间类型分区列为例,在动态分区功能中,我们支持了按特定时间周期自动创建新分区以容纳实时数据。对于实时的用户行为日志等场景该功能基本能够满足需求。但在一些更复杂的场景下,例如处理非实时数据时,分区列与当前系统时间无关,且包含大量离散值。此时为提高效率我们希望依据此列对数据进行分区,但数据实际可能涉及的分区无法预先掌握,或者预期所需分区数量过大。这种情况下动态分区或者手动创建分区无法满足我们的需求,自动分区功能很好地覆盖了此类需求。

1)语法

建表时,使用以下语法填充[CREATE-TABLE ]时的 partition_info 部分:

a、AUTO RANGE PARTITION

 CREATE TABLE `date_table` (
     `TIME_STAMP` datev2 NOT NULL
 ) ENGINE=OLAP
 DUPLICATE KEY(`TIME_STAMP`)
 
 AUTO PARTITION BY RANGE (date_trunc(`TIME_STAMP`, 'month'))
 (
 )
 DISTRIBUTED BY HASH(`TIME_STAMP`) BUCKETS 10
 PROPERTIES (
   "replication_allocation" = "tag.location.default: 1"
 );

b、AUTO LIST PARTITION

LIST 自动分区支持多个分区列,分区列写法同普通 LIST 分区一样: AUTO PARTITION BY LIST (col1, col2, …)`

CREATE TABLE `str_table` (
   `str` varchar not null
) ENGINE=OLAP
DUPLICATE KEY(`str`)

AUTO PARTITION BY LIST (`str`)
(
)
DISTRIBUTED BY HASH(`str`) BUCKETS 10
PROPERTIES (
  "replication_allocation" = "tag.location.default: 1"
);

2)使用限制

  • AUTO RANGE PARTITION

  • 分区函数仅支持 date_trunc,分区列仅支持 DATE 或者 DATETIME 类型;

  • NULL 值分区:,不支持 NULLABLE 列作为分区列

  • AUTO LIST PARTITION

    • 分区名长度不得超过 50. 该长度来自于对应数据行上各分区列内容的拼接与转义,因此实际容许长度可能更短。

    • 不支持函数调用,分区列支持 BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, LARGEINT, DATE, DATETIME, CHAR, VARCHAR 数据类型,分区值为枚举值。
    • 分区列的每个当前不存在对应分区的取值,都会创建一个独立的新 PARTITION
    • NULL 值分区:可以使用 NULLABLE 列作为分区列,会正常创建对应的 NULL 值分区:

3)使用场景

  • 处理非实时数据:当处理历史数据重放计算,例如处理过往某一年的数据且需要进行天级别的分区时,数据与当前系统时间无关,自动分区可根据数据中的分区列值自动创建分区,无需提前根据当前时间来规划分区。
  • 应对数据中存在大量离散值:如果分区列包含大量离散值,难以在建表或调整表结构时准确创建所需分区,自动分区能在数据导入时根据实际的离散值创建对应的分区,避免了手动创建大量分区的繁琐工作,也解决了动态分区无法处理离散值的问题。
  • 处理数据分布零散或难以预测的情况:当用户预期基于某列对表进行分区操作,但该列的数据分布比较零散或者难以预测时,自动分区可以在数据写入时自动创建对应的分区,无需提前精确规划分区范围和数量。

三、数据分桶

一个分区可以根据业务需求进一步划分为多个数据分桶(bucket)。每个分桶都作为一个物理数据分片(tablet)存储。合理的分桶策略可以有效降低查询时的数据扫描量,提升查询性能并增加并发处理能力。

1、分桶策略

1)Hash 分桶

在创建表或新增分区时,用户需选择一列或多列作为分桶列,并明确指定分桶的数量。在同一分区内,系统会根据分桶键和分桶数量进行哈希计算。哈希值相同的数据会被分配到同一个分桶中。

推荐在以下场景中使用 Hash 分桶:

  • 业务需求频繁基于某个字段进行过滤时,可将该字段作为分桶键,利用 Hash 分桶提高查询效率。
  • 当表中的数据分布较为均匀时,Hash 分桶同样是一种有效的选择。

例如,在下图中,p250102 分区根据 region 列被划分为 3 个分桶,哈希值相同的行被归入同一个分桶。

image-20250623162808940

示例中,通过 DISTRIBUTED BY HASH(region) 指定了创建·Hash 分桶,并选择 region 列作为分桶键。同时,通过 BUCKETS 8 指定了创建 8 个分桶。

CREATE TABLE demo.hash_bucket_tbl(
    oid         BIGINT,
    dt          DATE,
    region      VARCHAR(10),
    amount      INT
)
DUPLICATE KEY(oid)
PARTITION BY RANGE(dt) (
    PARTITION p250101 VALUES LESS THAN("2025-01-01"),
    PARTITION p250102 VALUES LESS THAN("2025-01-02")
)
DISTRIBUTED BY HASH(region) BUCKETS 8;

2)Random 分桶

在每个分区中,使用 Random 分桶会随机地将数据分散到各个分桶中,不依赖于某个字段的 Hash 值进行数据划分。Random 分桶能够确保数据均匀分散,从而避免由于分桶键选择不当而引发的数据倾斜问题。

在以下场景中,建议使用 Random 分桶:

  • 在任意维度分析的场景中,业务没有特别针对某一列频繁进行过滤或关联查询时,可以选择 Random 分桶;
  • 当经常查询的列或组合列数据分布极其不均匀时,使用 Random 分桶可以避免数据倾斜
  • Random 分桶无法根据分桶键进行剪裁,会扫描命中分区的所有数据,不建议在点查场景下使用;
  • 只有 DUPLICATE 表可以使用 Random 分区,UNIQUEAGGREGATE 表无法使用 Random 分桶(因为这里两个数据模型是有序的呀);
    • UNIQUE 表:主键约束,需保证数据唯一性,使用 Random 分桶,相同主键的数据可能被分配到不同分片
    • AGGREGATE 表的分桶列必须是聚合键的一部分,以确保相同聚合键的数据分布在同一分片中。若使用 Random 分桶,相同聚合键的数据可能被分散到不同分片,导致聚合结果错误

在导入数据时,单次导入作业的每个批次会被随机写入到一个 tablet 中,以此保证数据的均匀分布。例如,在一次操作中,8 个批次的数据被随机分配到 p250102 分区下的 3 个分桶中。

image-20250623163144527

示例中,通过 DISTRIBUTED BY RANDOM 语句指定了使用 Random 分桶,创建 Random 分桶无需选择分桶键,通过 BUCKETS 8 语句指定创建 8 个分桶。

CREATE TABLE demo.random_bucket_tbl(
    oid         BIGINT,
    dt          DATE,
    region      VARCHAR(10),
    amount      INT
)
DUPLICATE KEY(oid)
PARTITION BY RANGE(dt) (
    PARTITION p250101 VALUES LESS THAN("2025-01-01"),
    PARTITION p250102 VALUES LESS THAN("2025-01-02")
)
DISTRIBUTED BY RANDOM BUCKETS 8;

2、选择分桶键

分桶键可以是一列或者多列。如果是 DUPLICATE 表,任何 Key 列与 Value 列都可以作为分桶键。如果是 AGGREGATEUNIQUE 表,为了保证逐渐的聚合性,分桶列必须是 Key 列。

通常情况下,可以根据以下规则选择分桶键:

  • 利用查询过滤条件:使用查询中的过滤条件进行 Hash 分桶,有助于数据的剪裁,减少数据扫描量;
  • 利用高基数列:选择高基数(唯一值较多)的列进行 Hash 分桶,有助于数据均匀的分散在每一个分桶中;
  • 高并发点查场景:建议选择单列或较少列进行分桶。点查可能仅触发一个分桶扫描,不同查询之间触发不同分桶扫描的概率较大,从而减小查询间的 IO 影响。
  • 大吞吐查询场景:建议选择多列进行分桶,使数据更均匀分布。若查询条件不能包含所有分桶键的等值条件,将增加查询吞吐,降低单个查询延迟

3、选择分桶数量

Doris 中,一个 bucket 会被存储为一个物理文件(tablet)。一个表的 Tablet 数量等于 partition_num(分区数)乘以 bucket_num(分桶数)。一旦指定 Partition 的数量,便不可更改。

在确定 bucket 数量时,需预先考虑机器扩容情况。自 2.0 版本起,Doris 支持根据机器资源和集群信息自动设置分区中的分桶数。

1)手动设置分桶数

通过 DISTRIBUTED 语句可以指定分桶数量:

-- Set hash bucket num to 8
DISTRIBUTED BY HASH(region) BUCKETS 8

-- Set random bucket num to 8
DISTRIBUTED BY RANDOM BUCKETS 8

在决定分桶数量时,通常遵循数量与大小两个原则,当发生冲突时,优先考虑大小原则:

  • 大小原则:建议一个 tablet 的大小在 1-10G 范围内。过小的 tablet 可能导致聚合效果不佳,增加元数据管理压力;过大的 tablet 则不利于副本迁移、补齐,且会增加 Schema Change 操作的失败重试代价;
  • 数量原则:在不考虑扩容的情况下,一个表的 tablet 数量建议略多于整个集群的磁盘数量。

例如,假设有 10 台 BE 机器,每个 BE 一块磁盘,可以按照以下建议进行数据分桶:

单表大小 建议分桶数量 拆分 逻辑
500MB 4-8 个分桶 4-8 个分桶可在 10 台 BE 上均匀分布(每个 BE 承载 0.4-0.8 个分桶) 数据量小,分桶数过多会导致单个分桶数据量过小(<100MB),增加 IO 碎片化
5GB 6-16 个分桶 5GB / 16 分桶 ≈ 300MB / 分桶 兼顾并行度(16 分桶支持 16 线程并行扫描)和 IO 效率
50GB 32 个分桶 50GB / 32 分桶 ≈ 1.5GB / 分桶,适合机械硬盘的顺序 IO 性能(单个分桶扫描效率高) 32 分桶与 10 台 BE 配合时,每个 BE 承载 3-4 个分桶,负载均衡
500GB 建议分区,每个分区 50GB,每个分区 16-32 个分桶 500GB 拆分为 10 个 50GB 分区,每个分区 32 分桶,总桶数 320,避免单表分桶数过多 分区可按时间 / 业务维度隔离数据,分桶在分区内实现并行扫描
5TB 建议分区,每个分区 50GB,每个分桶 16-32 个分桶 5TB = 100 个 50GB 分区,每个分区 32 分桶,总桶数 3200 通过分区裁剪减少扫描范围(如查询只涉及 10 个分区,仅需扫描 320 个分桶)

技术约束与最佳实践

  1. 分桶数与 BE 节点数的关系
    • 理想分桶数 = BE 节点数 × 副本数 × 并发因子(通常取 1-2)。
    • 示例:10BE,副本数 3,则理想分桶数为 10 × 3 × 1=30,与 50GB 表建议的 32 分桶接近。
  2. 磁盘类型的影响
    • 机械硬盘(HDD):单个分桶建议 1-5GB(适合顺序 IO)。
    • 固态硬盘(SSD):单个分桶可缩小至 500MB-1GB(支持更高 IOPS)。
  3. 查询模式的适配
    • 点查(如主键查询):分桶数 ≈ BE 节点数,确保单分桶查询效率。
    • 全表扫描 / 聚合:分桶数可适当增加(如 BE 节点数 × 2),提升并行度。

2)自动设置分桶数

自动推算分桶数功能会根据过去一段时间的分区大小,自动预测未来的分区大小,并据此确定分桶数量。

在创建分桶时,可以通过 estimate_partition_size 属性来调整前期估算的分区大小。此参数为可选设置,若未给出,Doris 将默认取值为 10GB。请注意,该参数与后期系统通过历史分区数据推算出的未来分区大小无关。

-- Set hash bucket auto
DISTRIBUTED BY HASH(region) BUCKETS AUTO
properties("estimate_partition_size" = "20G")

-- Set random bucket auto
DISTRIBUTED BY HASH(region) BUCKETS AUTO
properties("estimate_partition_size" = "20G")

4、维护数据分桶

目前,Doris 仅支持修改新增分区的分桶数量,对于以下操作暂不支持:

  1. 不支持修改分桶类型
  2. 不支持修改分桶键
  3. 不支持修改已创建的分桶的分桶数量
  • 静态分区(手动创建)的分桶数由 DISTRIBUTED BY 子句决定
  • 动态分区(自动创建)的分桶数由 dynamic_partition.buckets 决定

在建表时,已通过 DISTRIBUTED 语句统一指定了每个分区的数量。为了应对数据增长或减少的情况,在动态增加分区时,可以单独指定新分区的分桶数量。以下示例展示了如何通过 ALTER TABLE 命令来修改新增分区的分桶数:

-- Modify hash bucket table
ALTER TABLE demo.hash_bucket_tbl 
ADD PARTITION p250103 VALUES LESS THAN("2025-01-03")
DISTRIBUTED BY HASH(region) BUCKETS 16;

-- Modify random bucket table
ALTER TABLE demo.random_bucket_tbl 
ADD PARTITION p250103 VALUES LESS THAN("2025-01-03")
DISTRIBUTED BY RANDOM BUCKETS 16;

-- Modify dynamic partition table
ALTER TABLE demo.dynamic_partition_tbl
SET ("dynamic_partition.buckets"="16");

ContactAuthor