4. Colocation Join 原理与实战
多表关联查询,最怕什么?数据 shuffle。数据在节点间搬来搬去,网络开销大,延迟高。Colocation Join 就是为了解决这个问题而生的。说白了,它让数据「原地不动」,计算跟着数据走。
核心思想:将相关表的数据按照相同的分布策略,预先存储在相同的节点上。查询时,各节点只扫描本地数据,无需跨节点传输。
4.1 Colocation Join 的概念与优势
我刚开始用 Doris 做多表 Join 时,最头疼的就是数据倾斜和网络开销。有一次跑一个 10 亿级的订单表关联用户表,光 shuffle 就花了 40 秒。后来换成 Colocation Join,同样的数据,关联耗时直接降到 5 秒以内。
Colocation Join 的核心机制,就是让参与 Join 的表,在物理存储层面就「对齐」。具体来说:
- 数据本地性:每个分桶的数据只存在于固定的节点上
- 分桶一致:关联表使用相同的分桶键和分桶数
- 零网络传输:Join 操作在本地完成,不产生数据移动
| 对比项 | 普通 Join | Colocation Join |
|---|---|---|
| 数据移动 | 需要 shuffle,跨节点传输 | 无需 shuffle,本地计算 |
| 网络开销 | 高,数据量大时明显 | 几乎为零 |
| 查询延迟 | 随数据量线性增长 | 稳定,受网络影响小 |
| 适用场景 | 任意关联 | 分桶键一致的关联 |
我的经验:Colocation Join 在星型模型和雪花模型中效果最明显。特别是事实表关联多个维度表时,只要维度表也按相同分桶键分布,性能提升非常可观。
4.2 Colocation Group 的创建与管理
Colocation Group 是 Colocation Join 的「容器」。同一 Group 内的表,Doris 会保证它们的数据分布完全一致。创建 Group 有两种方式:
4.2.1 自动创建 Group
最简单的方式,建表时指定相同的分桶键和分桶数,Doris 会自动将它们归入同一个 Colocation Group:
-- 创建订单表
CREATE TABLE orders (
order_id BIGINT,
user_id BIGINT,
order_date DATE,
amount DECIMAL(10,2)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3"
);
-- 创建用户表,分桶键和分桶数必须一致
CREATE TABLE users (
user_id BIGINT,
user_name VARCHAR(50),
city VARCHAR(20)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3"
);
这两张表会自动进入同一个 Colocation Group。Doris 会保证 user_id 相同的数据,一定落在同一个节点上。
4.2.2 手动指定 Group
如果你需要更精细的控制,可以手动指定 Group 名称:
CREATE TABLE orders (
order_id BIGINT,
user_id BIGINT,
order_date DATE,
amount DECIMAL(10,2)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"colocate_with" = "group_order_user"
);
CREATE TABLE users (
user_id BIGINT,
user_name VARCHAR(50),
city VARCHAR(20)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"colocate_with" = "group_order_user"
);
注意:手动指定 Group 时,必须确保所有表的分桶键、分桶数、副本数完全一致。否则 Doris 会报错,拒绝加入 Group。
4.2.3 查看和管理 Group
你可以通过以下命令查看当前集群中的 Colocation Group:
-- 查看所有 Colocation Group
SHOW PROC '/colocation_group';
-- 查看某个 Group 的详细信息
SHOW PROC '/colocation_group/group_order_user';
输出结果会显示 Group 内包含哪些表、每个表的分桶信息、以及当前 Group 的状态是否正常。
4.3 Colocation Join 的适用场景与限制
Colocation Join 不是万能的。我见过不少同学一上来就把所有表都设成 Colocation,结果反而出了问题。这里我总结一下它的适用场景和限制。
4.3.1 适用场景
- 星型模型关联:事实表关联多个维度表,维度表按事实表的分桶键分布
- 大表 Join 大表:两个大表按相同键关联,避免 shuffle 带来的网络瓶颈
- 实时写入 + 实时查询:数据持续写入,查询需要低延迟的场景
- 固定关联键:业务中 Join 条件始终是同一个字段
4.3.2 限制与避坑
我曾经踩过一个坑:把两个分桶数不同的表强行加入同一个 Group,结果 Doris 直接拒绝执行查询。后来查文档才发现,Colocation Group 对表的一致性要求非常严格。
| 限制项 | 说明 | 影响 |
|---|---|---|
| 分桶键必须一致 | 所有表使用相同的分桶键 | 无法灵活选择关联字段 |
| 分桶数必须一致 | 所有表使用相同的分桶数 | 数据量变化时调整困难 |
| 副本数必须一致 | 所有表使用相同的副本数 | 不同重要级别的表无法差异化配置 |
| 不支持动态修改 | Group 属性一旦创建,不能修改 | 需要重建表才能调整 |
| 不支持跨集群 | Colocation 只在同一集群内生效 | 跨集群关联无法利用此特性 |
避坑指南:我曾经遇到一个场景,订单表每天新增 500 万数据,分桶数设了 32。但用户表只有 100 万数据,也设了 32 个分桶。结果每个分桶里用户数据只有 3 万条,查询时扫描了很多空桶。后来我把用户表的分桶数调小,但 Colocation Group 要求分桶数一致,只能重建表。所以,建表前一定要评估好数据量。
4.4 实际案例配置
我们来看一个完整的实战案例。假设有一个电商系统,需要频繁查询「每个用户的订单总额和用户信息」。表结构如下:
-- 用户表:100 万用户
CREATE TABLE dim_user (
user_id BIGINT,
user_name VARCHAR(50),
register_date DATE,
city VARCHAR(20)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"colocate_with" = "ecommerce_group"
);
-- 订单表:每天 200 万订单,保留 30 天
CREATE TABLE fact_order (
order_id BIGINT,
user_id BIGINT,
order_date DATE,
amount DECIMAL(10,2),
status TINYINT
)
PARTITION BY RANGE(order_date) (
PARTITION p20240101 VALUES [('2024-01-01'), ('2024-01-02')),
PARTITION p20240102 VALUES [('2024-01-02'), ('2024-01-03'))
)
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"colocate_with" = "ecommerce_group"
);
-- 商品表:50 万商品
CREATE TABLE dim_product (
product_id BIGINT,
product_name VARCHAR(100),
category_id INT
)
DISTRIBUTED BY HASH(product_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"colocate_with" = "ecommerce_group"
);
注意,商品表的分桶键是 product_id,和用户表、订单表不同。这意味着商品表虽然加入了同一个 Group,但它的数据分布和另外两张表不一致。实际上,Colocation Group 只保证分桶键相同的表数据对齐。商品表的分桶键不同,它和订单表做 Join 时,仍然需要 shuffle。
关键点:Colocation Join 只对分桶键相同的表生效。如果 Join 条件不是分桶键,Doris 会自动退化为普通 Join。
现在,我们执行一个查询:统计每个用户的订单总额,并显示用户信息。
SELECT
u.user_id,
u.user_name,
SUM(o.amount) AS total_amount
FROM fact_order o
JOIN dim_user u ON o.user_id = u.user_id
WHERE o.order_date >= '2024-01-01'
GROUP BY u.user_id, u.user_name;
这个查询中,fact_order 和 dim_user 的分桶键都是 user_id,Doris 会在每个节点上本地完成 Join,无需跨节点传输数据。查询计划中会显示 COLOCATE 标识。
你可以通过 EXPLAIN 查看查询计划:
EXPLAIN SELECT
u.user_id,
u.user_name,
SUM(o.amount) AS total_amount
FROM fact_order o
JOIN dim_user u ON o.user_id = u.user_id
WHERE o.order_date >= '2024-01-01'
GROUP BY u.user_id, u.user_name;
如果看到类似 colocate: true 或 COLOCATE 字样,说明 Colocation Join 生效了。
我的习惯:每次建完 Colocation 表,我都会先跑一次 EXPLAIN,确认 Join 类型是 COLOCATE 而不是 SHUFFLE。这样可以提前发现问题,避免上线后性能翻车。
下面这张图展示了 Colocation Join 的核心流程:
从图中可以看到,Colocation Join 的核心就是「数据对齐 + 本地计算」。数据写入时就按分桶键分布好,查询时各节点独立完成 Join,最后合并结果。整个过程没有数据在网络间搬来搬去,性能自然就上去了。
再次提醒:Colocation Join 虽然好,但不要滥用。如果你的 Join 条件经常变化,或者分桶键不是主要的关联字段,强行使用 Colocation 反而会限制灵活性。我一般只在核心的、固定的关联场景中使用它。