详细对比 Doris 支持的四种 Join 执行方式:Broadcast Join、Shuffle Join、Bucket Shuffle Join 和 Colocate Join
在 Apache Doris 中,Join 查询的性能很大程度上取决于数据的分布和传输方式。为了在分布式环境下实现高效的关联查询,Doris 支持四种不同的 Join 执行方式:Broadcast Join、Shuffle Join、Bucket Shuffle Join 和 Colocate Join。
以下是对这四种 Join 方式的原理、触发条件、优缺点及适用场景的详细对比和分析。
一、 四种 Join 方式的原理与特点
1. Broadcast Join (广播连接)
- 原理:将 Join 的右表(通常是小表,称为 Build Table)的数据广播(复制)到含有左表(大表,称为 Probe Table)数据的所有 BE 节点上。每个 BE 节点在本地内存中构建哈希表,然后与本地的左表数据进行 Join。
- 数据移动:左表不动,右表全量广播。
- 网络开销:。
2. Shuffle Join (分区连接)
- 原理:当两张表都比较大时,Doris 会根据 Join Key 的 Hash 值,将两张表的数据同时重新分发(Shuffle)到各个 BE 节点上。相同 Join Key 的数据会被发送到同一个 BE 节点,然后在本地进行 Join。
- 数据移动:左右两表都需要网络传输(重新分发)。
- 网络开销:。
3. Bucket Shuffle Join (分桶哈希连接)
- 原理:这是 Doris 针对特定场景的优化。如果左表的分桶键(Bucket Key)正好是 Join Key,那么左表的数据就不需要 Shuffle(因为相同 Join Key 的数据已经天然在同一个 BE 节点上了)。Doris 只需将右表的数据按照左表的分桶规则进行 Shuffle,发送到对应的 BE 节点进行本地 Join。
- 数据移动:左表不动,右表根据左表的分桶规则进行 Shuffle。
- 网络开销:。
4. Colocate Join (同分布连接)
- 原理:最极致的优化。通过在建表时指定相同的 Colocate Group,保证两张表(或多张表)具有相同的分桶键、相同的分桶数,并且相同分桶的数据物理上存储在相同的 BE 节点上。在 Join 时,Doris 直接在本地节点进行 Join,完全没有网络传输。
- 数据移动:零网络传输,全本地化计算。
- 网络开销:。
二、 核心维度对比表
| 维度 | Broadcast Join | Shuffle Join | Bucket Shuffle Join | Colocate Join |
|---|---|---|---|---|
| 网络开销 | 中/高(随 BE 节点数线性增长) | 高(双表网络传输) | 低(仅单表网络传输) | 极低(零网络传输) |
| 内存开销 | 高(每个 BE 需容纳右表全量数据) | 中(节点仅容纳分片数据) | 中(节点仅容纳分片数据) | 低(全本地化流式处理) |
| 触发前提条件 | 右表足够小,能放入单 BE 内存 | 无特殊限制(通用机制) | Join Key 必须包含左表的分桶键 | 1. 两表在同一个 Colocate Group 2. Join Key 必须是分桶键 |
| 适用数据规模 | 大表 极小表 (如维度表) | 大表 大表 (无关联特征) | 大表 中/大表 | 大表 大表 (高频关联) |
| 对性能的影响 | 节点数多且右表偏大时,网络会成为瓶颈 | 容易受网络带宽限制,在大数据量下延迟高 | 相比 Shuffle Join 性能提升近一倍 | 性能最高,延迟最低,支持高并发 |
| 维护/设计成本 | 无(优化器自动选择) | 无(优化器自动选择) | 较低(建表时合理设计左表分桶键) | 较高(建表时需强制绑定 Group,限制后期扩容/数据重分布) |
三、 深入对比与场景分析
1. 为什么 Bucket Shuffle Join 优于 Shuffle Join?
- 减少了一半的网络传输:在标准 Shuffle 中,A 表和 B 表都要跨网络传输。而 Bucket Shuffle 中,A 表(左表)因为分桶键就是 Join Key,它已经在正确的节点上了,所以只有 B 表需要走网络。
- 减少了 CPU 开销:左表不需要重新计算 Hash 值和序列化,节省了 CPU。
- 适用场景:用户画像表(以
user_id分桶)关联行为明细表(以user_id分桶)。
2. Colocate Join 的威力与代价
Colocate Join 是 Doris 性能最强的 Join 方式,但它是有代价的:
- 建表约束:sql
-- 表1 CREATE TABLE table1 (k1 INT, v1 INT) DISTRIBUTED BY HASH(k1) BUCKETS 10 PROPERTIES("colocate_with" = "group_a"); -- 表2(必须与表1分桶键类型、分桶数、Group 名字完全一致) CREATE TABLE table2 (k1 INT, v2 INT) DISTRIBUTED BY HASH(k1) BUCKETS 10 PROPERTIES("colocate_with" = "group_a"); - 扩容影响:当集群扩容时,Doris 的 Colocate Manager 会在后台进行数据重平衡(Rebalance),以确保相同 Bucket 的数据仍然迁移到同一个新 BE 上。这期间可能会对写入性能产生短暂影响。
- 适用场景:数仓中的双主表关联,例如订单表和订单明细表(都按
order_id分桶),或者用户表和用户属性表。
四、 Doris 优化器(Nereids)的选择策略
在实际查询中,用户通常不需要手动指定使用哪种 Join(尽管可以通过 /*+ BROADCAST */ 或 /*+ SHUFFLE */ 的 Hint 来强制指定)。Doris 的新一代查询优化器(Nereids)会基于CBO(基于代价的优化)自动做出最优选择,选择逻辑大致如下:
- 检查是否满足 Colocate Join:如果两表属于同一 Colocate Group,且 Join 条件匹配分桶键,直接使用 Colocate Join。
- 检查是否满足 Bucket Shuffle Join:如果不满足 Colocate,但左表分桶键等于 Join Key,且右表大小合适,优先选择 Bucket Shuffle Join。
- 估算 Broadcast 与 Shuffle 的代价:
- 如果右表的数据量(经过 Filter 过滤后的行数和数据大小)非常小,优化器会选择 Broadcast Join。
- 如果右表数据量较大,或者 BE 节点数非常多导致 Broadcast 传输总量超过了 Shuffle,优化器会选择 Shuffle Join。
五、 总结与设计建议
- 数仓设计黄金法则:在设计 Doris 表结构时,尽量将高频作为 Join Key 的列(如
user_id,device_id,order_id)设为表的分桶键(Distribution Key)。这样即使无法实现 Colocate Join,也能白嫖到 Bucket Shuffle Join 的性能红利。 - 核心核心业务表:对于业务上最核心、关联最频繁的两三张超大表,务必在建表时使用
colocate_with属性将它们关联在一起,使用 Colocate Join。 - 维度表:对于配置表、字典表等小表,无需过多考虑分布,Doris 会自动通过 Broadcast Join 完美解决。