10 数据倾斜:诊断、根因与系统性解决方案

摘要

数据倾斜(Data Skew)是分布式计算的头号性能杀手,也是生产中 Spark 作业慢、OOM、长尾 Task 最常见的根因。其本质是:Shuffle 后某些分区的数据量远大于其他分区,导致处理该分区的 Task 成为整个 Stage 的瓶颈——整个 Stage 的完成时间 = 最慢那个 Task 的完成时间,其他 Task 早已完成在空等。第 06 篇介绍的 AQE Skew Join 自动处理了部分倾斜场景,但它有明确的边界:只能处理 SortMergeJoin 的输入倾斜,无法处理极端单 Key 倾斜、GROUP BY 阶段倾斜、多级 Join 中的传播倾斜。本文系统讲解数据倾斜的完整解决体系:如何诊断倾斜(Spark UI 方法论、SQL 探查技巧)、倾斜的四大根因分析、七种解决方案(Broadcast 强制、Salting、两阶段聚合、倾斜数据隔离、AQE Skew Join、Repartition 打散、分桶预处理)的适用场景与实现细节,以及多种方案在生产中的组合使用策略。


第 1 章 数据倾斜的诊断方法

1.1 通过 Spark UI 定位倾斜

当一个 Spark 作业运行缓慢时,数据倾斜的诊断流程:

步骤一:查看作业的 Stage 时间分布

打开 Spark UI → Jobs 标签页,找到耗时异常长的 Job,点击进入。在 Stages 列表中,找到耗时最长的 Stage(通常是包含 Shuffle Reduce 的 Stage)。

步骤二:查看 Stage 内的 Task 时间分布

点击进入该 Stage 的详情页,查看 Task 列表的指标:

  • Duration 列:如果大部分 Task 在 2 秒完成,但有 1-2 个 Task 耗时 30 分钟,这是典型的倾斜症状
  • Shuffle Read Size / Records 列:倾斜的 Task 读取的 Shuffle 数据量通常是其他 Task 的几十到几千倍
  • GC Time 列:倾斜 Task 往往伴随大量 GC(大数据量在内存中积压)
  • Spill (Memory)Spill (Disk):倾斜 Task 内存不足时会 Spill 到磁盘,导致额外 I/O 开销

典型倾斜 Task 的 UI 特征

Task 0:  Duration=2s,  Shuffle Read=52MB,   Records Read=500,000
Task 1:  Duration=2s,  Shuffle Read=48MB,   Records Read=480,000
Task 2:  Duration=2s,  Shuffle Read=55MB,   Records Read=520,000
...
Task 89: Duration=2s,  Shuffle Read=50MB,   Records Read=500,000
Task 90: Duration=31m, Shuffle Read=48GB,   Records Read=480,000,000  ← 倾斜!
Task 91: Duration=2s,  Shuffle Read=52MB,   Records Read=500,000

Task 90 的 Shuffle Read 是其他 Task 的约 1000 倍——显然是某个 Key 的数据都被 Hash 到了分区 90。

步骤三:定位倾斜的 Key

确认倾斜后,需要找到是哪个 Key 导致的倾斜:

-- 查找倾斜的 Join Key:找出哪些 key 出现次数异常多
SELECT join_key, COUNT(*) as cnt
FROM skewed_table
GROUP BY join_key
ORDER BY cnt DESC
LIMIT 20;
 
-- 典型输出:
-- join_key=NULL        cnt=50,000,000  ← NULL 集中到一个分区
-- join_key=hot_user    cnt=30,000,000
-- join_key=unknown     cnt=20,000,000
-- join_key=normal_a    cnt=100
-- join_key=normal_b    cnt=95
-- ...

1.2 倾斜的四大根因

根因一:业务 Key 本身分布不均(头部效应)

在电商、社交、内容平台中,数据天然呈幂律分布(Pareto 分布):少数热门用户、热门商品、热门话题贡献了绝大多数数据。

userId 分布:
  超级用户(直播平台主播):1 个用户有 5 亿条行为记录
  普通用户:平均每人 100 条行为记录
  5 亿 / 100 = 500 万倍的倾斜比

根因二:NULL 值集中

NULL 值在 Hash Partitioning 时通常被映射到同一个分区(hash(NULL) 是固定值)。如果某列有大量 NULL,会导致处理 NULL 分区的 Task 承担所有 NULL 行的数据。

-- 检查 NULL 值比例
SELECT
    COUNT(*) total,
    SUM(CASE WHEN join_key IS NULL THEN 1 ELSE 0 END) null_count,
    SUM(CASE WHEN join_key IS NULL THEN 1 ELSE 0 END) / COUNT(*) null_ratio
FROM table;
-- null_ratio = 0.30 意味着 30% 的数据都会落到 NULL 分区

根因三:数据过滤/Join 后的倾斜传播

上游 Stage 的倾斜可能在下游 Stage 被放大:

-- Stage 1:events JOIN dim_user(某些 userId 倾斜,但 JOIN 后数据量变大)
-- Stage 2:对 Stage 1 的结果再做 GROUP BY category
-- 如果倾斜的 userId 都属于同一 category,Stage 2 中 category 分区也倾斜

根因四:Shuffle Key 的 Hash 碰撞

极少见,但存在:两个不同的 Key 具有相同的 Hash 值,被分配到同一分区。当参与 Join 的两张表恰好大量 Key 都 Hash 到同一个分区时(小概率事件),也会产生倾斜。可以通过修改 spark.sql.shuffle.partitions 换一个分区数来缓解(改变 Hash 空间)。


第 2 章 解决方案一:Broadcast 强制(最简单有效)

2.1 核心原理

如果 Join 的一侧数据量小到可以 Broadcast,就根本不存在”分区倾斜”问题——Broadcast Join 把小表广播到每个 Executor,Probe 端(大表)在本地做 lookup,不触发 Shuffle,没有分区概念,自然没有倾斜。

适用条件:Join 的一侧(通常是维表)数据量足够小(能放入 Executor 内存),但当前 CBO 错误估算导致没有自动 Broadcast。

操作

-- 方法一:SQL Hint
SELECT /*+ BROADCAST(dim_user) */ a.*, b.name
FROM fact_table a
JOIN dim_user b ON a.userId = b.id;
 
-- 方法二:DataFrame API
import org.apache.spark.sql.functions.broadcast
val result = factDF.join(broadcast(dimUserDF), "userId")
 
-- 方法三:调大 Broadcast 阈值(全局生效,谨慎)
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "100MB")

为什么 Broadcast 能消除倾斜

SortMergeJoin 需要将大表和小表都按 Join Key 做 Shuffle,相同 Key 的数据落到同一分区。如果某个 Key(如 hot_user)在大表中有 5 亿行,这 5 亿行都 Hash 到同一分区 → 倾斜。

Broadcast Join 中,大表根本不做 Shuffle——每个 Executor 上大表的 Task 直接在本地与 Broadcast 的 HashMap 做 lookup。hot_user 的 5 亿行被分散到多个 Task(大表的 N 个分区),每个 Task 各自处理一小部分 hot_user 行,不存在集中到一个分区的问题。

生产避坑

Broadcast 有内存风险:如果被 Broadcast 的表实际很大(如 CBO 统计信息过时导致误判),Driver 收集全量数据可能 OOM,每个 Executor 持有完整副本也可能 OOM。建议为 autoBroadcastJoinThreshold 设置合理上限(不超过 Executor 内存的 20%),并开启 AQE 让运行时做二次检查。


第 3 章 解决方案二:Salting(加盐)技术

3.1 Salting 是什么,为什么出现

Salting(加盐) 是解决 Join 倾斜最经典的手动方案:在 Join Key 上添加随机前缀,将一个热点 Key 分散到多个”虚拟 Key”,使其均匀分布到多个分区。

“为什么叫盐”:类比密码学中在密码前加随机盐(Salt)来打破规律性——这里的目的是打破 Hash 的聚集性,让原本聚集的 Key 均匀散开。

不使用 Salting 会怎样:热点 Key(如 userId='hot_user')的所有数据都 Hash 到同一分区,单个 Task 处理数亿行,整个 Stage 等待这一个 Task,耗时可能是正常情况的 100 倍。

3.2 Salting 的实现步骤

fact_table JOIN dim_user ON fact_table.userId = dim_user.id 为例,userId='hot_user' 导致倾斜:

步骤一:对大表(事实表)添加随机盐

-- N 是盐的数量(并行度),通常 10-50
-- 大表每行 userId 加上随机 0 到 N-1 的前缀
SELECT
    CONCAT(CAST(FLOOR(RAND() * 10) AS STRING), '_', userId) AS userId_salted,
    amount,
    ts
FROM fact_table;
 
-- 结果:
-- original userId='hot_user' 被拆分为:
--   '0_hot_user', '1_hot_user', '2_hot_user', ..., '9_hot_user'
-- 10 种不同 Key,Hash 到 10 个不同分区

步骤二:对小表(维表)做笛卡尔积扩展

-- 维表每行复制 N 份,每份加不同的盐前缀
-- 这样 '0_hot_user', '1_hot_user', ... 都能在维表中找到匹配
SELECT
    CONCAT(CAST(salt.n AS STRING), '_', dim_user.id) AS id_salted,
    dim_user.name,
    dim_user.level
FROM dim_user
CROSS JOIN (SELECT explode(sequence(0, 9)) AS n) salt;
 
-- 结果:维表的每行被复制 10 份
-- id='hot_user' 被扩展为:
--   '0_hot_user', '1_hot_user', ..., '9_hot_user' 共 10 行

步骤三:用加盐后的 Key 做 Join

WITH salted_fact AS (
    SELECT CONCAT(CAST(FLOOR(RAND() * 10) AS STRING), '_', userId) AS userId_salted,
           amount, ts
    FROM fact_table
),
expanded_dim AS (
    SELECT CONCAT(CAST(n AS STRING), '_', id) AS id_salted, name, level
    FROM dim_user
    CROSS JOIN (SELECT explode(sequence(0, 9)) AS n) salt
)
SELECT f.amount, f.ts, d.name, d.level
FROM salted_fact f
JOIN expanded_dim d ON f.userId_salted = d.id_salted;

Salting 的效果

  • hot_user 的 5 亿行按随机盐均匀分配到 10 个分区,每个分区约 5000 万行
  • 维表膨胀了 10 倍(|dim_user| × 10),这是 Salting 的代价

代价分析:维表膨胀 N 倍意味着 Shuffle 数据量增加 N 倍(对维表侧)。当维表很小(如 100 万行 × 10 = 1000 万行,约 50MB),这个代价可以接受。当维表本身很大(如 10 亿行),Salting 会使 Join 的总数据量增大 N 倍,反而恶化性能——此时需要其他策略。

3.3 局部 Salting(只对热点 Key 加盐)

对所有 Key 加盐会使维表膨胀 N 倍,但实际上只有少数热点 Key 需要处理。局部 Salting 只对热点 Key 加盐,其他 Key 正常处理:

-- 1. 识别热点 Key(提前计算,写成常量列表)
-- hot_keys = ['hot_user_1', 'hot_user_2', 'bot_account']
 
-- 2. 将大表拆为热点行和普通行
CREATE TEMP VIEW fact_hot AS
SELECT CONCAT(CAST(FLOOR(RAND() * 10) AS STRING), '_', userId) AS userId_key,
       amount, ts, 'hot' AS row_type
FROM fact_table WHERE userId IN ('hot_user_1', 'hot_user_2', 'bot_account');
 
CREATE TEMP VIEW fact_normal AS
SELECT userId AS userId_key, amount, ts, 'normal' AS row_type
FROM fact_table WHERE userId NOT IN ('hot_user_1', 'hot_user_2', 'bot_account');
 
-- 3. 维表只对热点 Key 做扩展
CREATE TEMP VIEW dim_hot AS
SELECT CONCAT(CAST(n AS STRING), '_', id) AS id_key, name, level
FROM dim_user
CROSS JOIN (SELECT explode(sequence(0, 9)) AS n) salt
WHERE id IN ('hot_user_1', 'hot_user_2', 'bot_account');
 
CREATE TEMP VIEW dim_normal AS
SELECT id AS id_key, name, level FROM dim_user
WHERE id NOT IN ('hot_user_1', 'hot_user_2', 'bot_account');
 
-- 4. 分别 Join 后 UNION ALL 合并
SELECT f.amount, f.ts, d.name FROM fact_hot f JOIN dim_hot d ON f.userId_key = d.id_key
UNION ALL
SELECT f.amount, f.ts, d.name FROM fact_normal f JOIN dim_normal d ON f.userId_key = d.id_key;

局部 Salting 的维表膨胀只针对热点 Key(通常极少),整体代价远小于全量 Salting。


第 4 章 解决方案三:两阶段聚合(解决 GROUP BY 倾斜)

4.1 GROUP BY 倾斜的场景

Salting 主要解决 Join 倾斜,但 GROUP BY 阶段也会发生倾斜:

-- 如果 category='food' 有 5 亿行,GROUP BY category 时 food 分区异常大
SELECT category, SUM(amount)
FROM orders
GROUP BY category;

这类倾斜 Salting 无法直接解决(GROUP BY 没有另一侧可以扩展)。两阶段聚合(Two-Phase Aggregation) 是专门的解决方案。

4.2 两阶段聚合的原理

思路:先做一轮局部聚合(带随机盐),打散热点 Key 的数据;再做一轮全局聚合(去掉盐),合并局部聚合结果。

阶段一:加盐分区 + 局部聚合

-- 第一阶段:对 category 加随机盐,分散到 N 个分区
SELECT
    CONCAT(CAST(FLOOR(RAND() * 10) AS STRING), '_', category) AS category_salted,
    SUM(amount) AS partial_sum,
    COUNT(*) AS partial_count
FROM orders
GROUP BY CONCAT(CAST(FLOOR(RAND() * 10) AS STRING), '_', category);
 
-- 效果:'food' 的 5 亿行被分散到:
-- '0_food', '1_food', ..., '9_food' 各约 5000 万行
-- 每个分区单独做 SUM(局部聚合),无倾斜

阶段二:去盐 + 全局合并

-- 第二阶段:去掉盐前缀,对局部聚合结果做全局合并
SELECT
    regexp_replace(category_salted, '^[0-9]+_', '') AS category,
    SUM(partial_sum) AS total_amount,
    SUM(partial_count) AS total_count
FROM phase1_result
GROUP BY regexp_replace(category_salted, '^[0-9]+_', '');

两阶段聚合的效果

  • 第一阶段:热点 Key food 的 5 亿行被分散到 10 个分区,最大分区只有 5000 万行(不倾斜)
  • 第二阶段:只处理 10 个”food”的局部聚合结果(10 行),代价极小
  • 整体无倾斜

适用性:两阶段聚合适用于所有可交换可结合的聚合函数(SUM、COUNT、MAX、MIN、SUM+COUNT 推导 AVG)。对于不可分解的聚合(如 COUNT(DISTINCT col)COLLECT_LIST),两阶段聚合无法直接应用。

4.3 COUNT DISTINCT 的倾斜处理

COUNT(DISTINCT userId) 的去重聚合无法使用简单的两阶段聚合(局部 COUNT(DISTINCT) 再求和会重复计数)。特殊处理方法:

-- 方法一:改写为 GROUP BY + COUNT(先去重再聚合)
SELECT category, COUNT(*) AS distinct_users
FROM (
    SELECT DISTINCT category, userId
    FROM orders
) deduped
GROUP BY category;
 
-- 方法二:如果精确去重不是必须的,使用 HyperLogLog 近似(approx_count_distinct)
SELECT category, approx_count_distinct(userId, 0.05) AS approx_distinct_users
FROM orders
GROUP BY category;
-- 误差约 5%,但完全无倾斜问题(HLL 算法天然并行)

第 5 章 解决方案四:倾斜数据隔离处理

5.1 倾斜数据隔离的思想

对于极端单 Key 倾斜(一个 Key 贡献了绝大多数数据),Salting 可能收效有限——即使分散到 100 个分区,每个分区仍然有数百万行,仍然是其他正常 Key 分区的几十倍。

倾斜数据隔离(Skew Data Isolation) 的思路:

  1. 将热点 Key 的数据单独提取出来,用专门的、高并行度的方式处理
  2. 剩余的正常 Key 数据用常规方式处理
  3. 最后合并结果

5.2 实现示例

# 确定热点 Key(可以硬编码已知热点,或动态查询)
hot_keys = ['user_001', 'bot_account', 'unknown']
 
# 分离热点数据和正常数据
hot_df = fact_df.filter(col("userId").isin(hot_keys))
normal_df = fact_df.filter(~col("userId").isin(hot_keys))
 
# 热点数据:强制高并行度处理(repartition 打散)
hot_result = (
    hot_df
    .repartition(1000, col("userId"), monotonically_increasing_id())  # 组合分区键打散
    .join(broadcast(dim_user_df), "userId")  # 维表 Broadcast,避免热点 Join 倾斜
    .groupBy("category")
    .agg(sum("amount").alias("total"))
)
 
# 正常数据:常规处理
normal_result = (
    normal_df
    .join(dim_user_df, "userId")
    .groupBy("category")
    .agg(sum("amount").alias("total"))
)
 
# 合并
final_result = hot_result.union(normal_result) \
    .groupBy("category") \
    .agg(sum("total").alias("total"))

关键技巧:对热点数据使用 repartition(1000, col("userId"), monotonically_increasing_id())——加入 monotonically_increasing_id() 作为辅助分区键,确保即使 userId 相同,不同行也能被分配到不同分区(否则同一 userId 的所有行还是会落到同一分区)。


第 6 章 解决方案五:AQE Skew Join(自动处理)

6.1 AQE Skew Join 的适用边界回顾

第 06 篇已详细讲解 AQE Skew Join 的原理,这里重点讨论其边界:

有效场景

  • SortMergeJoin 的输入侧发生倾斜(Shuffle 完成后某分区数据量 > 256MB 且 > 中位数 5 倍)
  • 倾斜不是极端单 Key 导致的(AQE 可以拆分文件范围,但单 Key 的数据无法跨分区)

无效场景

  • BroadcastHashJoin 的 Probe 端倾斜(没有 Shuffle,AQE 无法感知分区倾斜)
  • GROUP BY 阶段倾斜(不是 Join 倾斜)
  • 极端单 Key 倾斜(拆分后每个子分区仍然只有这一个 Key 的数据,仍然倾斜)
  • 流处理场景(Structured Streaming)

AQE Skew Join 的参数确认

spark.sql.adaptive.skewJoin.enabled=true
spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes=256MB
spark.sql.adaptive.skewJoin.skewedPartitionFactor=5
# 倾斜分区拆分目标大小(越小拆分越多,并行度越高)
spark.sql.adaptive.advisoryPartitionSizeInBytes=64MB

6.2 AQE Skew Join + 手动方案的组合

在生产中,AQE Skew Join 和手动 Salting 通常是互补关系,而不是替代关系:

  • AQE Skew Join:处理不可预知的偶发倾斜(倾斜模式在不同批次间变化),无需人工干预
  • 手动 Salting/隔离:处理已知的、持续的、极端的倾斜(如特定业务 Key 总是倾斜),AQE 处理不了的极端场景

最佳实践:先开启 AQE Skew Join,观察是否能解决 80% 的倾斜问题;剩余 AQE 无法处理的极端倾斜,再用手动 Salting 或隔离处理。


第 7 章 解决方案六:NULL 值倾斜的特殊处理

7.1 NULL 倾斜的识别与处理

NULL 导致的倾斜有一个特殊性:NULL 在业务上通常是”未知”值,参与 Join 时是否有意义需要业务判断

-- NULL 的 Join 语义:NULL != NULL(SQL 标准)
-- SELECT * FROM a JOIN b ON a.key = b.key
-- 当 a.key IS NULL 时,不会匹配 b 中任何行(Inner Join 中 NULL 行被丢弃)

因此,对于 Inner Join,NULL 的数据在 Join 后会被丢弃。既然结果一样,可以在 Shuffle 前提前过滤掉 NULL 行

-- 在 Join 前过滤 NULL(减少 Shuffle 数据量,消除 NULL 分区倾斜)
SELECT a.userId, b.name, a.amount
FROM (SELECT * FROM fact_table WHERE userId IS NOT NULL) a  -- 提前过滤 NULL
JOIN dim_user b ON a.userId = b.id;

对于 Left Join 需要保留 NULL 行的场景:

-- 将 NULL 处理为特殊标识(避免集中到同一分区)
SELECT a.userId, b.name, a.amount
FROM (SELECT
    COALESCE(userId, CONCAT('null_', CAST(FLOOR(RAND() * 100) AS STRING))) AS userId_safe,
    amount
  FROM fact_table) a
LEFT JOIN dim_user b ON a.userId_safe = b.id;
-- NULL 行被分配了 'null_0' 到 'null_99' 的随机 Key,均匀分散到 100 个分区

第 8 章 解决方案七:分桶预处理(彻底根治)

8.1 分桶如何根治倾斜

第 05 篇讲到,Bucket Join 通过预先按 Join Key 分桶,让 Join 时完全不做 Shuffle。没有 Shuffle,就没有分区倾斜的问题——hot_user 的数据被分散到多个桶文件中(由 Hash 决定),每个桶文件被一个 Task 处理。

等等——如果 hot_user 的所有数据都 Hash 到同一个桶号,分桶后仍然是倾斜的?

是的,分桶本身并不解决倾斜,它只是消除了 Shuffle。如果桶内本身有倾斜,需要组合其他方案:

分桶 + 高并行度方案:将表分成更多的桶(如 1024 个),热点 Key 只占 1 个桶,其他 1023 个桶包含正常数据。对这 1 个热点桶单独处理(可以使用 Broadcast 或 Salting),其他桶正常 Bucket Join。

真正彻底的方案:在业务层面处理数据质量问题——热点 Key(如机器人账户、无效数据)应该在 ETL 最早阶段被清洗掉,而不是带着倾斜进入计算层。


第 9 章 各方案的对比与选择矩阵

方案适用倾斜类型代价是否需要修改代码自动化程度
Broadcast 强制Join 倾斜(一侧小)内存增加少量(加 Hint)半自动
Salting(全量)Join 倾斜(大表 × 中表)维表膨胀 N 倍需要改写 SQL手动
局部 SaltingJoin 倾斜(已知热点 Key)只对热点膨胀较复杂 SQL手动
两阶段聚合GROUP BY 倾斜一轮额外 Shuffle改写 SQL手动
倾斜数据隔离极端单 Key 倾斜代码复杂度高需要改写手动
AQE Skew JoinSortMergeJoin 输入倾斜Stage 间等待开销无需修改全自动
NULL 过滤/打散NULL 聚集倾斜极小少量 SQL 改写半自动
分桶预处理持续性 Join 倾斜写入时分桶开销建表时规划一次性配置

选择流程(优先级顺序)

1. 开启 AQE Skew Join → 观察是否解决
2. 检查是否可以 Broadcast 小侧 → 加 Hint
3. 检查是否有 NULL 倾斜 → 提前过滤或打散
4. 检查是否是已知热点 Key → 局部 Salting 或数据隔离
5. 检查是否是 GROUP BY 倾斜 → 两阶段聚合
6. 考虑分桶预处理(适合持续性高频查询)
7. 评估是否是数据质量问题 → 在 ETL 源头清洗热点数据

小结

数据倾斜是分布式计算的结构性问题,没有一劳永逸的通用方案,需要根据倾斜类型、数据规模、业务约束选择合适的组合策略:

  • 诊断:Spark UI 的 Task Duration 和 Shuffle Read Size 是最直接的倾斜信号;SQL 探查找出热点 Key
  • 四大根因:业务 Key 幂律分布、NULL 值集中、倾斜传播、Hash 碰撞
  • Broadcast:最简单有效,直接消灭倾斜分区的概念;受内存限制
  • Salting:将热点 Key 分散到多个虚拟 Key,均匀分布到多分区;代价是维表膨胀
  • 两阶段聚合:解决 GROUP BY 倾斜;先加盐做局部聚合,再去盐做全局合并
  • 倾斜数据隔离:对极端热点 Key 单独处理,其余正常流程
  • AQE Skew Join:全自动,SortMergeJoin 输入倾斜的最优先选择;有边界
  • NULL 处理:在 Shuffle 前过滤或打散 NULL 行
  • 分桶:从存储层根治持续性 Join 倾斜

第 11 篇将综合应用前 10 篇的所有知识,给出一份 Spark SQL 调优实战手册:从一条慢 SQL 的完整诊断流程,到各类典型慢查询的根因分析与修复方案,以及企业级 Spark SQL 调优的系统性方法论。


思考题

  1. Salting 技术通过在 Key 上追加随机前缀来打散热点数据,但随机前缀破坏了数据的关联性,因此 JOIN 的另一侧必须同步扩展。这个扩展操作本质上是一次数据复制,如果两侧都有倾斜怎么办?双侧 Salting 会带来什么组合爆炸问题?
  2. AQE 的 Skew Join 自动切分倾斜分区,触发条件是分区大小超过中位数的 skewFactor 倍且超过 skewedPartitionThresholdInBytes。但 AQE 只对 SortMergeJoin 生效,对 BroadcastHashJoin 无效。在什么情况下一个本应走 BroadcastHashJoin 的查询,反而应该主动强制使用 SortMergeJoin 以便让 AQE Skew 优化介入?
  3. NULL 值倾斜是一类特殊的数据倾斜,常见于 JOIN Key 包含大量 NULL 的场景。SQL 标准规定 NULL != NULL,因此 NULL Key 不会匹配任何行——但这些 NULL 行依然会被 Hash 分配到同一个分区(hashCode(null) = 0)造成倾斜。在 OUTER JOIN 的场景下,NULL Key 行不能直接丢弃,应如何在保证语义正确的前提下处理这种倾斜?

参考资料