StarRocks表设计之数据分布策略

本文介绍StarRocks数据表设计中的数据分布策略和应用实践。

一、数据分布问题

现代分布式数据存储系统的数据分布问题是指:系统如何在系统内分布式节点上划分、组织、安排数据?数据分布问题是影响系统性能的核心问题之一。常见的数据分布方式有如下几种:Round-Robin、Range、List 和 Hash。

  • Round-Robin:以轮询的方式把数据逐个放置在相邻节点上。
  • Range:按区间进行数据分布。如图所示,区间 [1-3]、[4-6] 分别对应不同的范围 (Range)。
  • List:直接基于离散的各个取值做数据分布,性别、省份等数据就满足这种离散的特性。每个离散值会映射到一个节点上,多个不同的取值可能也会映射到相同节点上。
  • Hash:通过哈希函数把数据映射到不同节点上。

为了更灵活地划分数据,除了单独采用上述数据分布方式之外,也可以根据具体的业务场景需求组合使用这些数据分布方式。常见的组合方式有 Hash+Hash、Range+Hash、Hash+List。

二、StarRocks的数据分布

StarRocks采用了分区+分桶两级数据分布策略。第一层级为分区。表中数据可以根据分区列(通常是时间和日期)分成一个个更小的数据管理单元。查询时,通过分区裁剪,可以减少扫描的数据量,显著优化查询性能。第二层级为分桶。同一个分区中的数据通过分桶,划分成更小的数据管理单元。并且分桶以多副本形式(默认为3)均匀分布在 BE 节点上,保证数据的高可用。

创建数据表时,通过设置合理的分区和分桶,可以实现数据的均匀分布和查询性能的提升。数据均匀分布是指数据按照一定规则划分为子集,并且均衡地分布在不同节点上。查询时能够有效裁减数据扫描量,最大限度地利用集群的并发性能,从而提升查询性能。因此在设计数据表时,要充分考虑数据的分区和分桶策略。 一个简单的例子,设计一个记录站点访问量的聚合表,以访问日期为分区,以访问时间和站点id分桶,数据表创建语句如下:

js 复制代码
CREATE TABLE site_access_stat ( 
event_time DATETIME, 
site_id INT DEFAULT '10', 
city_code VARCHAR(100), 
user_name VARCHAR(32) DEFAULT '', 
pv BIGINT SUM DEFAULT '0' 
) 
AGGREGATE KEY(event_time, site_id, city_code, user_name) 
-- 设为分区方式为表达式分区,并且使用时间函数的分区表达式 
PARTITION BY date_trunc('day', event_time) 
-- 设置分桶方式为哈希分桶,必须指定分桶键 
DISTRIBUTED BY HASH(event_time, site_id);

三、分区和分桶策略

3.1 分区

分区用于将数据划分成不同的区间。分区的主要作用是将一张表按照分区键拆分成不同的管理单元,针对每一个管理单元选择相应的存储策略,如分桶数、冷热策略、存储介质、副本数等。StarRocks 支持在一个集群内使用多种存储介质,您可以将新数据所在分区放在 SSD 盘上,利用 SSD 优秀的随机读写性能来提高查询性能,将旧数据存放在 SATA 盘上,以节省数据存储的成本。

StarRocks目前支持表达式分区、Range 分区和 List 分区,或者不分区(即全表只有一个分区)。

从 v3.1 版本开始,表达式分区、Range 分区和 List 分区均支持所有表类型。

  • 表达式分区:原称自动创建分区,适用大多数场景,并且灵活易用。适用于按照连续日期范围或者枚举值来查询和管理数据。开发者仅需要在建表时使用分区表达式,即可实现导入数据时自动创建分区,不需要预先创建出分区或者配置动态分区属性。(注:从 v3.4 开始,表达式分区方式进一步得到优化,统一所有分区策略,并支持更复杂的解决方案。在大多数情况下,建议您使用表达式分区。表达式分区将在未来版本中逐渐取代其他分区策略。)
  • Range 分区:典型的场景是数据简单有序,并且通常按照连续日期/数值范围来查询和管理数据。比如一些特殊场景,历史数据需要按月划分分区,而最近数据需要按天划分分区。Range分区需要动态、批量或者手动创建。
  • List 分区:典型的场景是按照枚举值来查询和管理数据,并且一个分区中需要包含各分区列的多值。比如经常按照国家和城市来查询和管理数据,则可以使用该方式,选择分区列为 city,一个分区包含属于一个国家的多个城市的数据。List分区需要手动创建。StarRocks 会按照您显式定义的枚举值列表与分区的映射关系将数据分配到相应的分区中。

选择分区列和分区粒度

分区键由一个或者多个分区列组成。选择合理的分区列可以有效的裁剪查询数据时扫描的数据量。业务系统中⼀般会选择根据时间进行分区,以优化大量删除过期数据带来的性能问题,同时也方便冷热数据分级存储,此时可以使用时间列作为分区列进行表达式分区或者 Range 分区。此外,如果经常按照枚举值查询数据和管理数据,则可以选择枚举值的列作为分区列进行表达式分区或者 List 分区。

选择分区单位时需要综合考虑数据量、查询特点、数据管理粒度等因素。

  • 表单月数据量很小,可以按月分区,相比于按天分区,可以减少元数据数量,从而减少元数据管理和调度的资源消耗。
  • 表单月数据量很大,而大部分查询条件精确到天,如果按天分区,可以做有效的分区裁剪,减少查询扫描的数据量。
  • 数据要求按天过期,可以按天分区。

3.2 分桶

在一个分区中,必须进行分桶。一个分区被分成了多个桶 bucket,每个桶的数据称之为一个 tablet。 StarRocks支持的分桶方式有哈希分桶和随机分桶。哈希分桶支持所有表类型;随机分桶从 v3.1 版本开始支持,仅支持明细表。创建数据表时如果没有指定分桶方式,则默认为随机分桶。

  • 随机分桶:建表和新增分区时无需设置分桶键。在同一分区内,数据随机分布到不同的分桶中。 对每个分区的数据,StarRocks 将数据随机地分布在所有分桶中,适用于数据量不大,对查询性能要求不高的场景。如果您不设置分桶方式,则默认由 StarRocks 使用随机分桶,并且自动设置分桶数量。不过值得注意的是,如果查询海量数据且查询时经常使用一些列会作为条件列,随机分桶提供的查询性能可能不够理想。在该场景下建议您使用哈希分桶,当查询时经常使用这些列作为条件列时,只需要扫描和计算查询命中的少量分桶,则可以显著提高查询性能。

    随机分桶的使用限制:

    • 仅支持明细表。
    • 不支持指定 Colocation Group。
    • 不支持 Spark Load。
  • 哈希分桶:建表和新增分区时需要指定分桶键。在同一分区内,数据按照分桶键划分分桶后,所有分桶键的值相同的行会唯一分配到对应的一个分桶。 对每个分区的数据,StarRocks 会根据分桶键和分桶数量进行哈希分桶。在哈希分桶中,使用特定的列值作为输入,通过哈希函数计算出一个哈希值,然后将数据根据该哈希值分配到相应的桶中。

    哈希分桶的优点:

    • 提高查询性能。相同分桶键值的行会被分配到一个分桶中,在查询时能减少扫描数据量。
    • 均匀分布数据。通过选取较高基数(唯一值的数量较多)的列作为分桶键,能更均匀的分布数据到每一个分桶中。

    如何选择分桶键?

    • 假设存在列同时满足高基数和经常作为查询条件,则建议您选择其为分桶键,进行哈希分桶。 如果不存在这些同时满足两个条件的列,则需要根据查询进行判断。
    • 如果查询比较复杂,则建议选择高基数的列为分桶键,保证数据在各个分桶中尽量均衡,提高集群资源利用率。
    • 如果查询比较简单,则建议选择经常作为查询条件的列为分桶键,提高查询效率。
    • 并且,如果数据倾斜情况严重,您还可以使用多个列作为数据的分桶键,但是建议不超过 3 个列。

    注意事项:

    • 建表时,如果使用哈希分桶,则必须指定分桶键。
    • 组成分桶键的列仅支持整型、DECIMAL、DATE/DATETIME、CHAR/VARCHAR/STRING 数据类型。
    • 自 v3.2 起,建表后支持通过 ALTER TABLE 修改分桶键。

3.3 分桶数量

分桶数据量默认由 StarRocks 自动设置分桶数量(自 v2.5.7),同时也支持手动设置分桶数量。如果表中单个分区原始数据规模预计超过 100 GB,建议您手动设置分区中分桶数量。确定分区中分桶数量的方式:

  • 策略1:首先预估每个分区的数据量,然后按照每 10 GB 原始数据一个 Tablet 计算,从而确定分区中分桶数量。
  • 策略2:如果希望充分发挥并行性能可以设置为:BE数量 * CPU core/2,最好tablet控制在1GB左右,tablet太少并行度可能不够,太多可能远数据过多,底层scan并发太多性能下降。

3.4 分区与分桶的区别

  • 分区允许 StarRocks 在查询时通过分区裁剪跳过整个数据块,并启用仅元数据的生命周期操作,如删除旧数据或特定租户的数据。
  • 分桶则有助于将数据分布在多个 tablet 上,以并行化查询执行和均衡负载,特别是在与哈希函数结合使用时。

四、建表后的数据分布优化

随着业务场景中查询模式和数据量变化,建表时设置的分桶方式和分桶数量,以及排序键可能不再能适应新的业务场景,导致查询性能下降,此时可以通过 ALTER TABLE 调整分桶方式和分桶数量,以及排序键,优化数据分布。比如:

  • 分区中数据量增多,增加分桶数量

当按天分区的分区数据量相比原来变大很多,原本的分桶数量不再合适时,可以加大分桶数量,以让每个 Tablet 的大小一般控制在 1 GB ~ 10 GB。

  • 通过调整分桶键,来避免数据倾斜

当发现原有分桶键会导致数据倾斜(比如原来的分桶键只有 k1 一列),可以设置更合适的列、或者加入更多一些列到分桶键中。

  • 如果表为主键表,当业务的查询模式有较大变化,经常需要用到表中另外几个列作为条件列时,则可以调整排序键。

五、应用实践

5.1 经验总结:

  • 数据倾斜

如果业务场景中单独采用倾斜度大的列做分桶,很大程度会导致访问数据倾斜,那么建议采用多列组合的方式进行数据分桶。

  • 高并发

分区和分桶应该尽量覆盖查询语句所带的条件,这样可以有效减少扫描数据,提高并发。

  • 高吞吐

尽量把数据打散,让集群以更高的并发扫描数据,完成相应计算。

需要注意的是元数据管理,当Tablet 过多时会增加 FE/BE 的元数据管理和调度的资源消耗。

5.2 实战案例

案例1:商品销售表

某些热销产品数据量太大,利用产品分桶,会产生数据倾斜问题。热销产品加随机数打散分桶。

js 复制代码
-- 场景:商品销售表,热门商品销量远高于普通商品
CREATE TABLE product_sales (
  `product_id` bigint NOT NULL COMMENT '商品ID',
  `sale_date` date NOT NULL COMMENT '销售日期',
  `sales_count` bigint NOT NULL COMMENT '销售数量',
  `sales_amount` decimal(10,2) NOT NULL COMMENT '销售金额'
) ENGINE=OLAP
AGGREGATE KEY(`product_id`, `sale_date`)
COMMENT '商品销售表'
PARTITION BY `sale_date`
-- 解决策略:热点值单独处理+动态分桶调整
DISTRIBUTED BY HASH (
  CASE 
    WHEN product_id IN (1001, 1002, 1003) THEN concat(product_id, '_', rand()%5)
    ELSE product_id
  END
);    

案例2:电商订单数据表

某电商订单数据表设计,经常会按日期、订单状态、订单渠道、支付方式的条件进行数据查询,因此在设计分桶时,把status,sales_channel,pay_type作为分桶键

js 复制代码
CREATE TABLE `orders` (
  `id` bigint NOT NULL COMMENT '订单ID',
  `order_no` varchar(64) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `status` tinyint NOT NULL COMMENT '订单状态:0-待付款,1-已付款,2-已发货,3-已完成,4-已取消,5-退款中,6-已退款',
  `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
  `pay_amount` decimal(10,2) NOT NULL COMMENT '实付金额',
  `freight_amount` decimal(10,2) NOT NULL COMMENT '运费金额',
  `discount_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '折扣金额',
  `sales_channel` tinyint NOT NULL COMMENT '销售渠道:1-官网,2-App,3-小程序,4-第三方平台',
  `pay_type` tinyint DEFAULT NULL COMMENT '支付方式:1-支付宝,2-微信,3-银行卡',
  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
  `delivery_time` datetime DEFAULT NULL COMMENT '发货时间',
  `receive_time` datetime DEFAULT NULL COMMENT '确认收货时间',
  `comment_time` datetime DEFAULT NULL COMMENT '评价时间',
  `receiver_name` varchar(32) NOT NULL COMMENT '收货人姓名',
  `receiver_phone` varchar(20) NOT NULL COMMENT '收货人电话',
  `receiver_address` varchar(255) NOT NULL COMMENT '收货人地址',
  `note` varchar(500) DEFAULT NULL COMMENT '订单备注',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '更新时间'
) ENGINE=OLAP
PRIMARY KEY(`id`)
COMMENT '电商订单表'
PARTITION BY date_trunc('day', `create_time`)
DISTRIBUTED BY HASH (`user_id`,`status`,`sales_channel`,`pay_type`);

案例3:高并发用户行为日志

js 复制代码
-- 场景:高吞吐量用户行为日志,每日数据量达数亿条
-- 需要支持每秒数万条写入和高并发查询
CREATE TABLE high_throughput_user_behavior (
  `event_id` bigint NOT NULL COMMENT '事件ID',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `event_time` datetime NOT NULL COMMENT '事件时间',
  `event_date` date NOT NULL COMMENT '事件日期(用于分区)',
  `event_hour` tinyint NOT NULL COMMENT '事件小时(0-23)',
  `event_type` tinyint NOT NULL COMMENT '事件类型:1-点击,2-浏览,3-购买,4-分享',
  `page_id` int NOT NULL COMMENT '页面ID',
  `device_id` varchar(64) NOT NULL COMMENT '设备ID',
  `app_version` varchar(32) COMMENT '应用版本',
  `channel` tinyint COMMENT '渠道ID',
  `province` varchar(32) COMMENT '省份',
  `city` varchar(32) COMMENT '城市',
  `duration` int COMMENT '停留时长(秒)'
) ENGINE=OLAP
DUPLICATE KEY(`event_id`, `event_time`)
COMMENT '高吞吐量用户行为日志表'
-- 1. 分区策略:按日分区
PARTITION BY `event_date`
-- 2. 分桶策略:多维度哈希分桶,最大化数据打散
DISTRIBUTED BY HASH (`user_id`, `event_hour`, `device_id`) BUCKETS 1024
PROPERTIES (
  -- 3. 写入优化参数
  "replication_num" = "2",                   -- 适当减少副本数提高写入性能
  "dynamic_partition.enable" = "true",       -- 启用动态分区
  "dynamic_partition.time_unit" = "DAY",     -- 按日分区
  "dynamic_partition.start" = "-15",         -- 保留15天数据
  "dynamic_partition.end" = "2",             -- 预创建2天分区
  "dynamic_partition.prefix" = "p",
  "dynamic_partition.buckets" = "1024",
  -- 4. 存储优化参数
  "storage_format" = "columnar",             -- 列存格式
  "disable_auto_compaction" = "false",       -- 启用自动压缩
  "compaction_policy" = "tiered",            -- 分层压缩策略
  "max_file_size" = "1073741824",            -- 最大文件大小1GB
  "min_file_size" = "10485760",              -- 最小文件大小10MB
  -- 5. 计算优化参数
  "enable_vectorized_engine" = "true",       -- 启用向量执行引擎
  "parallel_exec_instance_num" = "64",       -- 并行执行实例数
  "pipeline_dop" = "8",                      -- 每个管道的并行度
  "batch_size" = "4096",                     -- 批处理大小
  -- 6. 高并发写入优化
  "write_buffer_size" = "134217728",         -- 写入缓冲区大小128MB
  "flush_thread_num" = "16",                 -- 刷盘线程数
  "max_write_mbytes_per_sec" = "0",          -- 不限制写入带宽
  "enable_unique_key_merge_on_write" = "false" -- 关闭写入时合并
);
相关推荐
FreeCode4 小时前
StarRocks表设计之表类型的选择
大数据
isfox1 天前
Hadoop 1.x 与 2.x 版本对比:架构演进与核心差异解析
大数据
货拉拉技术1 天前
货拉拉离线大数据跨云迁移-综述篇
大数据·云原生
Lx3521 天前
Hadoop与实时计算集成:Lambda架构实践经验
大数据·hadoop
武子康1 天前
大数据-101 Spark Streaming 有状态转换详解:窗口操作与状态跟踪实战 附多案例代码
大数据·后端·spark
expect7g1 天前
COW、MOR、MOW
大数据·数据库·后端
武子康2 天前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
阿里云大数据AI技术2 天前
2025云栖大会·大数据AI参会攻略请查收!
大数据·人工智能
代码匠心2 天前
从零开始学Flink:数据源
java·大数据·后端·flink