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,要么跑十几个小时。其实,换个思路,用对分桶和排序,效果天差地别。

核心认知:大表 Join 大表的本质不是「比谁内存大」,而是「比谁数据组织得好」。分桶 + 排序,就是给数据做「预组织」。
Bucket Map Join vs Sort Merge Bucket Join 核心流程 Bucket Map Join 前提:两表按相同字段分桶,桶数成倍数关系 每个 Map Task 只加载对应桶的小表部分 在 Map 端完成局部 Join,无需 Reduce 优点:无 Shuffle,速度快 缺点:桶内数据无序,需额外哈希匹配 Sort Merge Bucket Join 前提:两表分桶且桶内数据已排序 每个 Map Task 加载对应桶,利用排序做归并 Map 端归并 Join,无需 Reduce 优点:归并效率高,内存占用低 优点:适合超大表等值 Join 关键区别 Bucket Map Join 依赖哈希匹配;SMB Join 依赖排序归并,后者在超大表场景下更稳定

4.1 Bucket Map Join:给数据分个桶

Bucket Map Join 的核心思想很简单:把大表拆成小份。

具体做法是,两个表按照 Join key 做哈希分桶,桶数成倍数关系。比如左表分 16 个桶,右表分 4 个桶。这样每个桶内的数据量就小多了。

然后每个 Map Task 只加载自己负责的那个桶。右表对应桶的数据可以塞进内存,在 Map 端直接做哈希 Join。

我的经验:分桶数怎么定?我一般建议桶内数据量控制在 100MB-200MB 左右。太小了 Map Task 太多,调度开销大;太大了又塞不进内存。这个范围是我在几个 TB 级项目里试出来的,比较稳。

配置上也很简单,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;
注意:Bucket Map Join 要求两个表的分桶字段必须和 Join key 一致。我曾经见过有人分桶字段和 Join key 不一样,结果优化没生效,任务跑了 3 个小时。检查了半天才发现是分桶字段写错了。

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;
一句话总结:Bucket Map Join 是「分桶 + 哈希」,SMB Join 是「分桶 + 排序 + 归并」。后者更稳,但建表时多了一步排序。

4.3 大表 Join 大表的完整优化思路

好了,前面讲了具体技术。现在咱们拉高视角,看看大表 Join 大表到底该怎么思考。

我个人习惯把优化思路分成四个层次,从简单到复杂:

层次 策略 适用场景 代价
第一层 数据过滤 + 列裁剪 所有场景,先做再说 低,只需改 SQL
第二层 分桶 + Bucket Map Join 两表分桶字段与 Join key 一致 中,需重建表
第三层 分桶 + 排序 + SMB Join 超大表等值 Join,数据倾斜不严重 较高,建表 + 排序开销
第四层 分而治之 + 中间表 + 增量计算 极端大表,或业务允许拆分 高,需改业务逻辑

第四层我多说两句。有些场景下,两个表实在太大,即使分桶 + SMB 也扛不住。这时候就得换个思路:能不能不 Join?

举个例子。我在一个电商项目中遇到过,订单表和用户行为表都是 TB 级,每天全量 Join 一次要跑 6 小时。后来我们改成按天增量 Join,每天只处理新增数据,然后合并到中间结果表。跑一次从 6 小时降到了 20 分钟。

避坑指南:我曾经在 SMB Join 上踩过一个坑——两个表的分桶数不匹配。左表 16 桶,右表 8 桶,结果 Hive 没走 SMB,退化成普通 Reduce Join,跑了 4 个小时。后来我养成了习惯,每次建表都检查分桶数是否成倍数关系。

4.4 什么时候选哪种?

我整理了一个决策表,方便你快速判断:

条件 推荐方案 原因
小表 < 1GB Map Join 直接塞内存,最简单
两表都大,但分桶字段匹配 Bucket Map Join 无 Shuffle,速度快
两表都大,分桶且排序 SMB Join 归并效率高,内存低
两表都大,无法分桶 Reduce Join + 倾斜优化 兜底方案,配合 Skew Join
极端大表,业务允许拆分 增量计算 + 中间表 从架构层面解决问题

嗯,到这里 Join 优化的核心内容就差不多了。从 Map Join 到 SMB Join,再到架构层面的分而治之,其实思路是递进的——能提前做的准备,就不要留到运行时。分桶、排序、增量计算,都是在给运行时减负。

下次你遇到大表 Join 大表,别急着调内存参数。先问问自己:数据组织好了吗?