GBase 8a
性能调优
文章
精选

GBase 8a 表设计实战:分布键、分区、复制表选型指南

发表于2026-03-25 09:24:14142次浏览3个评论

 

很多性能问题在建表那一刻就已经埋下了。本文从实际场景出发,系统讲解 GBase 8a 的表设计决策,附带反例和优化手段。


一、数据分布基础:数据是怎么存到各节点的

GBase 8a 是一个 Shared-Nothing 架构,数据水平切分后分散存储在各 gnode 上。切分方式取决于建表时指定的分布键(Distribution Key)

CREATE TABLE orders (
    order_id    BIGINT NOT NULL,
    customer_id INT    NOT NULL,
    dept_id     INT,
    amount      DECIMAL(18,2),
    order_date  DATE
) DISTRIBUTED BY HASH(customer_id);
--                   ↑ 分布键

GBase 8a 使用 Hash 函数将分布键的值映射到对应节点。同一个 customer_id 的所有行一定在同一个 gnode 上

如果不指定 DISTRIBUTED BY,系统默认使用第一列作为分布键,这通常不是我们想要的。


二、分布键选择的核心原则

原则 1:高基数(Cardinality)

分布键的唯一值越多,数据在各节点分布越均匀。

唯一值估算适合做分布键?
gender2~3❌ 严重倾斜
province~34❌ 节点少时倾斜明显
user_id千万级✅ 分布均匀
order_id十亿级✅ 最均匀

原则 2:是高频 JOIN 的关联键

如果 ordersorder_items 经常按 order_id 做 JOIN,把 order_id 作为双方的分布键,JOIN 时无需跨节点数据 Shuffle:

-- 父表
CREATE TABLE orders (
    order_id BIGINT, ...
) DISTRIBUTED BY HASH(order_id);

-- 子表
CREATE TABLE order_items (
    item_id  BIGINT,
    order_id BIGINT, ...
) DISTRIBUTED BY HASH(order_id);  -- 与父表保持一致

此时 orders JOIN order_items ON orders.order_id = order_items.order_id本地 JOIN,性能最优。

原则 3:不要用日期或时间列做分布键

日期列的唯一值数量有限(如 order_date 按天只有 365 个值),Hash 分布后容易不均匀,而且日期列几乎不会出现在 JOIN 条件里。


三、分区(Partition):与分布键的区别

分布键决定数据去哪个节点,分区决定数据在节点内部的文件组织方式。两者互不干扰,可以组合使用。

GBase 8a 支持 Range 分区

CREATE TABLE orders (
    order_id   BIGINT,
    order_date DATE,
    amount     DECIMAL(18,2)
) DISTRIBUTED BY HASH(order_id)
PARTITION BY RANGE(order_date) (
    PARTITION p2023 VALUES LESS THAN ('2024-01-01'),
    PARTITION p2024 VALUES LESS THAN ('2025-01-01'),
    PARTITION p2025 VALUES LESS THAN ('2026-01-01'),
    PARTITION pmax  VALUES LESS THAN MAXVALUE
);

分区的主要收益是分区裁剪(Partition Pruning):查询带上分区键条件时,扫描范围从整张表缩减到对应分区:

-- 只扫描 p2024 分区,跳过其他所有分区
SELECT * FROM orders WHERE order_date BETWEEN '2024-06-01' AND '2024-06-30';

什么时候该用分区?

  • 表非常大(单节点数据量 > 数十 GB)
  • 查询有明显的时间范围过滤
  • 历史数据需要定期删除(ALTER TABLE DROP PARTITION p2023DELETE 快几个数量级)

什么时候不需要分区?

  • 查询基本是全表扫描
  • 表数据量不大(< 1 亿行)
  • 分区数量过多(> 1000),反而增加元数据开销

四、复制表(Replicated Table):小维度表的最优策略

对于字典表、维度表等行数少、很少变更的表,最好将其建为复制表:

CREATE TABLE dim_product (
    product_id   INT,
    product_name VARCHAR(128),
    category     VARCHAR(64)
) REPLICATED;

复制表的每个 gnode 上都保存完整数据副本。当事实表与复制表 JOIN 时,不需要任何网络传输,直接本地 JOIN:

-- orders 是分布表,dim_product 是复制表
-- 每个 gnode 直接用本地的 dim_product 数据与本地的 orders 数据 JOIN
SELECT o.order_id, p.product_name, o.amount
FROM orders o
JOIN dim_product p ON o.product_id = p.product_id;

复制表的适用边界

条件建议
行数 < 100 万,几乎不更新✅ 复制表
行数 100~1000 万,偶尔更新⚠️ 谨慎,更新代价高
行数 > 1000 万❌ 用分布表 + 合理的分布键

复制表的写入操作(INSERT/UPDATE/DELETE)需要在每个 gnode 上都执行一遍,代价比分布表高。高频写入的表不要用复制表。


五、数据类型选择注意事项

GBase 8a 是列存储引擎,数据类型的选择直接影响压缩率和查询性能。

字符串类型

-- 错误示范:用 VARCHAR 存固定格式的 ID
status VARCHAR(20)  -- 只存 'active'/'inactive'

-- 正确做法:用 TINYINT 存枚举,VARCHAR 存描述
status TINYINT      -- 0=inactive, 1=active

对于真正变长的字段,VARCHARCHAR 更节省空间。列存压缩对低基数的字符串(如状态码、省份)效果极好。

数值类型

  • 整数优先选 INTBIGINT,不要用 DECIMAL(20,0) 存整数
  • 金额字段用 DECIMAL(18,2),不要用 DOUBLE(浮点精度问题)
  • 如果需要存纳秒级时间戳,用 BIGINT 自行管理,TIMESTAMP 精度到秒

时间类型

-- DATETIME 存完整时间,适合日志类数据
create_time DATETIME

-- DATE 存日期,如果只关心天不关心时分秒
order_date DATE

-- 避免用 VARCHAR 存日期,无法利用分区裁剪和日期函数优化
order_date VARCHAR(10)  -- ❌

六、一张实际建表案例

以电商订单场景为例,综合运用上述原则:

-- 事实表:大表,用高基数列做分布键,按时间分区
CREATE TABLE orders (
    order_id     BIGINT      NOT NULL COMMENT '订单ID',
    customer_id  INT         NOT NULL COMMENT '用户ID(分布键)',
    product_id   INT         NOT NULL COMMENT '商品ID',
    dept_id      SMALLINT    NOT NULL COMMENT '部门ID',
    amount       DECIMAL(18,2)       COMMENT '订单金额',
    status       TINYINT     NOT NULL COMMENT '0:待支付 1:已支付 2:已取消',
    order_date   DATE        NOT NULL COMMENT '下单日期(分区键)',
    create_time  DATETIME             COMMENT '创建时间'
) DISTRIBUTED BY HASH(customer_id)
PARTITION BY RANGE(order_date) (
    PARTITION p2024q1 VALUES LESS THAN ('2024-04-01'),
    PARTITION p2024q2 VALUES LESS THAN ('2024-07-01'),
    PARTITION p2024q3 VALUES LESS THAN ('2024-10-01'),
    PARTITION p2024q4 VALUES LESS THAN ('2025-01-01'),
    PARTITION p2025   VALUES LESS THAN ('2026-01-01'),
    PARTITION pmax    VALUES LESS THAN MAXVALUE
);

-- 维度表:小表,复制表
CREATE TABLE dim_product (
    product_id   INT          NOT NULL,
    product_name VARCHAR(128) NOT NULL,
    category     VARCHAR(64),
    brand        VARCHAR(64)
) REPLICATED;

CREATE TABLE dim_dept (
    dept_id   SMALLINT     NOT NULL,
    dept_name VARCHAR(64)  NOT NULL
) REPLICATED;

七、常见的建表反模式

反模式后果正确做法
不指定分布键默认第一列,很可能倾斜明确 DISTRIBUTED BY HASH(合理列)
用 gender/status 做分布键数据严重倾斜,1~2 个节点承担所有负载换高基数列
维度表用分布表每次 JOIN 都 Hash Redistribute改为 REPLICATED
VARCHAR(255) 存枚举值压缩率低,占用更多内存用 TINYINT/SMALLINT
过多分区(>1000)元数据开销大,规划查询慢按季度或年分区,不要按天

总结

好的表设计是 GBase 8a 性能优化的起点,后期改分布键需要重建表,代价极高。建议在设计阶段就回答以下三个问题:

  1. 这张表主要被哪些 SQL 访问?JOIN 条件是什么? → 决定分布键
  2. 查询是否有明显的时间范围过滤?数据量多大? → 决定是否分区
  3. 这张表数据量有多少?写入频率如何? → 决定是否用复制表

评论

登录后才可以发表评论
GBase用户47954发表于 1个月前
感谢作者的精彩分享!
流泪猫猫头发表于 1个月前
很详细实用的文章。
gbase_poc_凡夫发表于 18天前
是这样 distributed by('a'),而不是DISTRIBUTED BY HASH(customer_id)吧。