4、Join优化策略(下):Bucket Map Join、Sort Merge Bucket Join、大表Join大表优化思路
上一讲我们聊了 Map Join 和它的几个变种。说白了,那些都是「以大欺小」的思路——把小表塞进内存,让 Map 阶段就把活儿干完。
但现实往往没那么美好。你想想看,两个表都是几百 GB 甚至 TB 级,谁也塞不进内存,怎么办?
这时候就得请出今天的主角:Bucket Map Join 和 Sort Merge Bucket Join(简称 SMB Join)。
我在项目中遇到过不少团队,一提到大表 Join 大表就只会调 mapreduce.memory.mb,结果要么 OOM,要么跑十几个小时。其实,换个思路,用对分桶和排序,效果天差地别。
4.1 Bucket Map Join:给数据分个桶
Bucket Map Join 的核心思想很简单:把大表拆成小份。
具体做法是,两个表按照 Join key 做哈希分桶,桶数成倍数关系。比如左表分 16 个桶,右表分 4 个桶。这样每个桶内的数据量就小多了。
然后每个 Map Task 只加载自己负责的那个桶。右表对应桶的数据可以塞进内存,在 Map 端直接做哈希 Join。
配置上也很简单,Hive 里这样写:
-- 建表时指定分桶
CREATE TABLE orders_bucketed (
order_id INT,
user_id INT,
amount DOUBLE
)
CLUSTERED BY (user_id) INTO 16 BUCKETS;
CREATE TABLE users_bucketed (
user_id INT,
name STRING
)
CLUSTERED BY (user_id) INTO 4 BUCKETS;
-- 开启 Bucket Map Join
SET hive.optimize.bucketmapjoin = true;
-- 执行 Join
SELECT /*+ MAPJOIN(u) */ *
FROM orders_bucketed o
JOIN users_bucketed u
ON o.user_id = u.user_id;
4.2 Sort Merge Bucket Join:排序 + 归并 = 稳
Bucket Map Join 虽然好,但有个问题:桶内数据是无序的。Join 的时候还得做哈希匹配,内存占用还是不小。
SMB Join 更进一步——桶内数据也排好序。这样两个桶之间做 Join 时,就像合并两个有序链表一样,用归并的方式逐条匹配。
归并的好处是什么?
- 内存占用极低:只需要缓存当前比较的几条记录,不用把整个桶塞进内存
- 没有 Shuffle:所有工作在 Map 端完成,Reduce 阶段直接省掉
- 稳定:数据量再大也不怕,归并操作是流式的
配置示例:
-- 建表时指定分桶 + 排序
CREATE TABLE orders_sorted_bucketed (
order_id INT,
user_id INT,
amount DOUBLE
)
CLUSTERED BY (user_id) SORTED BY (user_id) INTO 16 BUCKETS;
CREATE TABLE users_sorted_bucketed (
user_id INT,
name STRING
)
CLUSTERED BY (user_id) SORTED BY (user_id) INTO 4 BUCKETS;
-- 开启 SMB Join
SET hive.optimize.bucketmapjoin = true;
SET hive.optimize.bucketmapjoin.sortedmerge = true;
SET hive.input.format = org.apache.hadoop.hive.ql.io.BucketizedHiveInputFormat;
-- 执行 Join
SELECT *
FROM orders_sorted_bucketed o
JOIN users_sorted_bucketed u
ON o.user_id = u.user_id;
4.3 大表 Join 大表的完整优化思路
好了,前面讲了具体技术。现在咱们拉高视角,看看大表 Join 大表到底该怎么思考。
我个人习惯把优化思路分成四个层次,从简单到复杂:
| 层次 | 策略 | 适用场景 | 代价 |
|---|---|---|---|
| 第一层 | 数据过滤 + 列裁剪 | 所有场景,先做再说 | 低,只需改 SQL |
| 第二层 | 分桶 + Bucket Map Join | 两表分桶字段与 Join key 一致 | 中,需重建表 |
| 第三层 | 分桶 + 排序 + SMB Join | 超大表等值 Join,数据倾斜不严重 | 较高,建表 + 排序开销 |
| 第四层 | 分而治之 + 中间表 + 增量计算 | 极端大表,或业务允许拆分 | 高,需改业务逻辑 |
第四层我多说两句。有些场景下,两个表实在太大,即使分桶 + SMB 也扛不住。这时候就得换个思路:能不能不 Join?
举个例子。我在一个电商项目中遇到过,订单表和用户行为表都是 TB 级,每天全量 Join 一次要跑 6 小时。后来我们改成按天增量 Join,每天只处理新增数据,然后合并到中间结果表。跑一次从 6 小时降到了 20 分钟。
4.4 什么时候选哪种?
我整理了一个决策表,方便你快速判断:
| 条件 | 推荐方案 | 原因 |
|---|---|---|
| 小表 < 1GB | Map Join | 直接塞内存,最简单 |
| 两表都大,但分桶字段匹配 | Bucket Map Join | 无 Shuffle,速度快 |
| 两表都大,分桶且排序 | SMB Join | 归并效率高,内存低 |
| 两表都大,无法分桶 | Reduce Join + 倾斜优化 | 兜底方案,配合 Skew Join |
| 极端大表,业务允许拆分 | 增量计算 + 中间表 | 从架构层面解决问题 |
嗯,到这里 Join 优化的核心内容就差不多了。从 Map Join 到 SMB Join,再到架构层面的分而治之,其实思路是递进的——能提前做的准备,就不要留到运行时。分桶、排序、增量计算,都是在给运行时减负。
下次你遇到大表 Join 大表,别急着调内存参数。先问问自己:数据组织好了吗?