公司真题库

【字节跳动】Spark Join 的三种实现与选型

91学AI·2026/7/20·7 阅读

考察点

这道题出自字节跳动大数据工程师一面,是 Spark SQL 执行原理的核心题。面试官想看你能不能把三种 join 的物理执行过程讲清楚,并且结合数据量级给出选型依据——大表 join 小表为什么 broadcast、两个大表为什么 sort merge 是兜底、shuffle hash 为什么逐渐边缘化。追问常往"broadcast 阈值怎么调"、"怎么从执行计划看出用了哪种 join"、"sort merge 的 spill 是怎么回事"走。

参考答案

Broadcast Hash Join:没有 shuffle 的 join

适用前提是有一张小表。Driver 把小表数据收集起来,构建成一个 hash map(key 是 join 字段),序列化后广播到所有 executor;大表完全不 shuffle,每个 map task 拿自己分区的数据在本地 hash map 里探测匹配。因为大表免掉了 shuffle 的写盘、网络传输、排序,这是性能最好的 join,没有之一。

触发条件是 Spark 估算小表尺寸小于 spark.sql.autoBroadcastJoinThreshold(默认 10MB)。生产上这个值常调到 100MB 甚至更大,前提是 driver 和 executor 内存够——广播的数据要先进 driver 内存构建,再分发到各 executor 的广播变量区,OOM 风险主要在 driver。可以用 /*+ BROADCAST(t) */ hint 或 broadcast(df) 强制指定。

一个易踩的坑:Spark 的尺寸估算基于统计信息,表没 analyze 过或者经过 filter 后估算失真,该 broadcast 的没走 broadcast。这时候要么跑 ANALYZE TABLE ... COMPUTE STATISTICS,要么手动 hint。

Sort Merge Join:大表 join 大表的默认兜底

两个表都放不进内存时走它,也是 Spark 的默认 join。三个阶段:shuffle 阶段,两边都按 join key 做 hash 分区重分布,保证相同 key 落到同一个 partition;sort 阶段,每个 partition 内两边数据各自按 key 排序;merge 阶段,两边有序的流做归并,指针往前走输出匹配行——因为排过序,不需要把所有数据加载进内存的 hash 表,内存占用可控,排不下的部分溢写磁盘(spill)。

它的问题是成本高:两次 shuffle 读写、排序的 CPU 开销。但这个"贵"是稳定的贵,不依赖数据能不能装进内存,所以是通用性最强的方案。排序这步还带来一个隐藏收益:如果数据本身已经按 join key 有序(比如分桶表 bucket table),可以跳过 shuffle 和 sort,直接 merge——这就是分桶表优化的原理。

Shuffle Hash Join:逐渐边缘化的中间派

流程是:两表按 join key shuffle 分区后,每个 partition 内把小的一侧建成 hash map,大的一侧流式探测。相比 sort merge 它省掉了排序,理论上更快;但代价是要把一侧的 partition 数据整个放进内存建 hash 表,单个 partition 数据量大时直接 OOM——它没有 sort merge 那种 spill 兜底能力。

历史上它在"某侧分区后足够小但超过 broadcast 阈值"的场景有一席之地。现在地位很尴尬:Spark 3 默认 spark.sql.join.preferSortMergeJoin=true,且 AQE 能在运行时把小表动态转 broadcast,shuffle hash 的适用窗口被两头挤压,实际执行计划里越来越少见。面试时讲清它被淘汰的原因,比背它的流程更能加分。

怎么选:决策树

选型逻辑可以归纳成一棵决策树。先看有没有一侧能广播:能(真实数据量,不是估算)→ broadcast hash join。不能广播 → 看是不是常规大表 join:是 → sort merge join,这是默认且最稳的。特殊场景:join key 高频复用、两表都超大 → 考虑分桶表,把 shuffle 成本一次性预付。最后 shuffle hash 基本不用主动选,了解原理应付追问即可。

倾斜问题要和 join 选型联动考虑:sort merge 遇到倾斜 key 会卡死在个别 task,Spark 3 AQE 的 skew join 优化(spark.sql.adaptive.skewJoin.enabled)能把过大的 partition 自动切开,开 AQE 是现代 Spark 做 join 的标配。

从执行计划验证

答完原理加一句"怎么验证"会显得很实战。df.explain() 或 Web UI 的 SQL 页面看物理计划:BroadcastHashJoin 会带 BroadcastExchange 节点;SortMergeJoin 前面有 Exchange hashpartitioning + Sort;ShuffleHashJoin 则是 Exchange 后直接 join 没有 Sort。线上排查 join 慢,第一步永远是看物理计划走的是不是预期的策略——估算失真导致走了 sort merge 而不是 broadcast,是最常见的 join 性能问题。

可能的追问

  • broadcast 时 driver OOM 怎么办?说明小表实际没想象中小。要么调回阈值走 sort merge,要么先对小表做聚合/filter 瘦身,要么给 driver 加内存——但根本上要质疑"这张表真的算小表吗"。
  • sort merge join 内存里需要什么?归并阶段只需两边当前行的缓冲,内存占用很小,主要内存压力在 sort 阶段(ExternalSorter 有 spill 机制),所以它能处理任意大的输入。
  • AQE 对 join 还有什么优化?除了 skew join 拆分,还能在运行时根据实际 shuffle 数据量把 sort merge 动态降级为 broadcast(运行后发现某侧其实很小),以及自动合并过小的分区。

评论 (0)

暂无评论,快来抢沙发吧!

91学AI

© 2026 91学AI · 按岗位学 AI 与大数据. All rights reserved.