订阅更新

大表JOIN慢到爆?聊聊MapJoin和数据倾斜

SQL手册 第 8 题 约 8 分钟读完

本文目录
  1. 总结
  2. 面试原题
  3. 先理解:为什么大表JOIN会慢
  4. 优化一:MapJoin(小表广播)
  5. 优化二:分区裁剪
  6. 优化三:数据倾斜处理
  7. 面试高频追问

总结

优化方法适用场景效果
------------------------
MapJoin / Broadcast一大一小表消除Shuffle,性能提升10x+
分区裁剪表有分区字段减少扫描量,基本功
过滤NULLJOIN Key有大量NULL减少无效Shuffle
加盐打散热点Key倾斜均匀分布计算负载
AQE自适应Spark 3.x自动优化,兜底方案

记住一个优先级:先裁剪 → 再尝试MapJoin → 最后处理倾斜。 80%的JOIN性能问题,前两步就能解决。

明天 DAY 9 聊漏斗分析的SQL写法,从埋点数据到转化率计算,这是业务分析岗和数据开发岗都会考的重要考点。

你遇到过最严重的数据倾斜有多夸张?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

前面7天聊的都是"怎么把SQL写对",从今天开始聊"怎么把SQL跑快"。JOIN优化是大数据面试的必考点——你的SQL逻辑正确但跑了2小时没出结果,在生产环境就是事故。

面试原题

你有一张10亿行的用户行为日志表和一张50万行的用户维度表,JOIN查询特别慢,你怎么优化?

先理解:为什么大表JOIN会慢

在Hive/Spark里,大部分JOIN都需要Shuffle——把两张表中相同JOIN Key的数据发送到同一个节点上进行匹配。

Shuffle的开销主要在两方面:磁盘IO(中间数据要落盘)和网络IO(数据要跨节点传输)。10亿行的表做Shuffle,数据传输量可能有几十GB,慢是必然的。

所以优化JOIN的核心思路就是:能不Shuffle就不Shuffle,必须Shuffle就减少Shuffle的数据量。

优化一:MapJoin(小表广播)

如果两张表中有一张足够小(通常小于几百MB),可以把小表直接广播到每个计算节点的内存中,大表不需要Shuffle,直接在Map端完成JOIN。

-- Hive中显式开启MapJoin
SET hive.auto.convert.join=true;  -- 默认就是true
SET hive.mapjoin.smalltable.filesize=25000000;  -- 小表阈值,默认25MB
 
SELECT /*+ MAPJOIN(dim_user) */
  t.user_id,
  t.event_name,
  t.event_time,
  d.user_level,
  d.register_channel
FROM user_event_log t
JOIN dim_user d ON t.user_id = d.user_id
WHERE t.dt = '2025-01-01'

Spark中对应的叫Broadcast Join:

-- Spark SQL
SET spark.sql.autoBroadcastJoinThreshold=104857600;  -- 100MB
 
SELECT /*+ BROADCAST(dim_user) */
  t.user_id,
  t.event_name,
  d.user_level
FROM user_event_log t
JOIN dim_user d ON t.user_id = d.user_id

我在做实时数仓的时候,用户维度表有50万行大约200MB,用Broadcast Join之后,原来要跑15分钟的查询1分钟就出结果了,性能提升了10倍以上。

注意事项:

小表不能太大,否则每个节点都要存一份,内存吃不消

Spark默认阈值是10MB,实际工作中可以调到100MB-500MB

LEFT JOIN时,小表必须在右边(被广播的那边)

优化二:分区裁剪

这是最容易忽略但效果最明显的优化。JOIN之前先用WHERE过滤掉不需要的数据:

-- ❌ 先JOIN再过滤,10亿行全量参与JOIN
SELECT *
FROM user_event_log t
JOIN dim_user d ON t.user_id = d.user_id
WHERE t.dt = '2025-01-01'
 
-- ✅ 先过滤再JOIN,只有当天的数据参与JOIN
SELECT *
FROM (
  SELECT * FROM user_event_log WHERE dt = '2025-01-01'
) t
JOIN dim_user d ON t.user_id = d.user_id

在Hive中,如果WHERE条件能命中分区字段(dt是分区键),优化器会自动做分区裁剪。但不是所有场景都能自动优化,养成子查询先过滤的习惯比较稳。

我的经验是:在Hive/Spark里写SQL,第一件事就是想清楚每张表需要扫描多少数据,能加分区过滤就加。这个意识面试官非常看重。

优化三:数据倾斜处理

这是JOIN优化里最难也最常考的部分。

什么是数据倾斜?

JOIN的时候,某个Key的数据量远大于其他Key。比如user_id为NULL的占了总量的30%,或者某个大客户的行为日志特别多。Shuffle之后,这个Key对应的节点要处理30%的数据,其他节点早就跑完了,整个任务卡在这一个节点上。

方案1:过滤NULL值

-- 如果NULL值没有业务意义,先过滤掉
SELECT *
FROM user_event_log t
JOIN dim_user d ON t.user_id = d.user_id
WHERE t.user_id IS NOT NULL
  AND d.user_id IS NOT NULL

我在做广告归因的时候遇到过,user_event_log里有15%的记录user_id为NULL(端上没有登录的匿名用户),这些记录JOIN不上维度表,但参与了Shuffle,白白浪费了大量资源。加一个NULL过滤,任务时间直接从40分钟降到12分钟。

方案2:加盐打散(热点Key处理)

如果某个非NULL的Key就是特别大(比如某个大客户有1000万条记录),用加盐的方式打散:

-- 大表:给热点Key加随机后缀
SELECT
  CASE
    WHEN user_id = 'big_customer_001'
    THEN CONCAT(user_id, '_', CAST(FLOOR(RAND() * 10) AS STRING))
    ELSE user_id
  END AS user_id_salt,
  event_name,
  event_time
FROM user_event_log
 
-- 小表:膨胀10倍,生成对应后缀
SELECT
  CONCAT(user_id, '_', suffix) AS user_id_salt,
  user_level,
  register_channel
FROM dim_user
LATERAL VIEW EXPLODE(ARRAY('0','1','2','3','4','5','6','7','8','9')) t AS suffix
WHERE user_id = 'big_customer_001'
 
UNION ALL
 
SELECT
  user_id AS user_id_salt,
  user_level,
  register_channel
FROM dim_user
WHERE user_id != 'big_customer_001'

原理:大表的热点Key被打散成10份,小表对应膨胀10倍来匹配。10个节点分担原来1个节点的工作量。

这个方案面试的时候完整写出来很加分,说明你不仅知道原理,还能落地。

方案3:两阶段JOIN

思路类似两阶段聚合,先用MapJoin处理小表中有的Key,再单独处理大表中小表没有的Key。这个方案比较复杂,面试中知道思路即可。

面试高频追问

Q:怎么判断JOIN发生了数据倾斜?

看Spark UI或者YARN的Task监控。如果大部分Task都在几分钟内完成,但有1-2个Task跑了几十分钟甚至几小时,基本就是数据倾斜了。在Spark UI的Stage详情页面能看到每个Task处理的数据量,倾斜的Task数据量会远大于平均值。

Q:Hive和Spark对JOIN的优化有什么区别?

Spark比Hive聪明很多。Spark有AQE(Adaptive Query Execution),能在运行时根据数据量自动选择JOIN策略、自动处理倾斜分区。Spark 3.x以后,很多以前需要手动优化的场景,AQE都能自动搞定。但面试官问你,你还是得知道手动优化的方法,因为AQE不是万能的。

-- Spark 3.x 开启AQE
SET spark.sql.adaptive.enabled=true;
SET spark.sql.adaptive.skewJoin.enabled=true;
刷题进度保存在当前浏览器