Spark SQL 性能优化:从定位问题到解决数据倾斜

Spark SQL 性能优化:从定位问题到解决数据倾斜

一份面向数据工程师的实战指南,覆盖 Spark SQL 任务优化的完整链路。


目录

  1. 理解 Spark SQL 执行过程
  2. 如何定位性能瓶颈
  3. 通用优化策略
  4. 数据倾斜:识别与处理
  5. 配置调优指南
  6. 实战案例复盘
  7. 优化检查清单

1. 理解 Spark SQL 执行过程

优化之前,先搞清楚一段 SQL 在 Spark 中到底经历了什么。

1.1 从 SQL 到 RDD 的五步走

SQL 文本
  → 解析(Parser)→ AST 语法树
    → 分析(Analyzer)→ 解析后的逻辑计划(绑定表名、列名、类型)
      → 优化(Optimizer)→ 优化后的逻辑计划(谓词下推、列裁剪、常量折叠)
        → 物理计划(Planner)→ 多个物理执行计划
          → 代价模型(CBO)→ 选出最优物理计划,生成 RDD DAG

关键点:Spark 的 Catalyst 优化器会自动做很多事,但前提是你的 SQL 写法给了它优化空间。

1.2 两个核心概念

概念 说明 代价
窄依赖 子 RDD 的每个分区只依赖父 RDD 的一个分区(map、filter、union) 无 Shuffle,快
宽依赖 子 RDD 的一个分区依赖父 RDD 的多个分区(groupBy、join、distinct) 触发 Shuffle,慢

优化的一条主线就是:减少 Shuffle 次数和数据量。

1.3 什么操作会触发 Shuffle

GROUP BY          → Shuffle
JOIN (非广播)      → Shuffle
COUNT(DISTINCT)   → Shuffle(且往往需要两次)
WINDOW FUNCTION   → 部分场景 Shuffle
UNION / UNION ALL → 无 Shuffle(仅合并)

1.4 Exchange 类型

在 Spark 执行计划中看到 Exchange 节点,说明发生了数据重新分区:

Exchange 类型 分区方式 典型触发操作
HashPartitioning hash(key) % numPartitions GROUP BYJOIN
RoundRobinPartitioning 轮询分配 DISTRIBUTE BY random()
RangePartitioning 按范围分配 ORDER BYWINDOW RANGE
BroadcastExchange 广播到所有节点 mapjoin / broadcast hint

2. 如何定位性能瓶颈

2.1 Spark UI —— 第一入口

打开 Spark UI(默认 driver:4040),按以下顺序排查:

Stage 页面(最重要)

┌─────────────────────────────────────────────────────────┐
│  Stage 3 (Shuffle stage)                                 │
│  Duration: 12min  ← 这个 Stage 为什么这么久?              │
│  Input: 200GB     ← 输入量合理吗?                         │
│  Shuffle Read: 80GB  ← Shuffle 数据量对吗?               │
│  Tasks: 200 total, 1 still running  ← 有长尾任务!        │
└─────────────────────────────────────────────────────────┘

关键指标:

指标 怎么看 问题信号
Duration Stage 总耗时 占总 Job 时间的比重
Tasks 总任务数 / 完成数 少量任务长时间不完成 → 数据倾斜
Shuffle Read/Write 数据量 过大说明聚合不够或分区不合理
Spill 溢写磁盘量 Spill 大 → 内存不足或 key 太多
GC Time 垃圾回收时间 > 10% → 内存压力大

如何判断数据倾斜(Stage 页面)

展开 Stage → 看 Summary Metrics 表的 Duration 列:

Task ID   Duration    Shuffle Read
0         15s          2MB
1         14s          2.1MB
...
198       12min        1.8GB    ← 倾斜!比其他 task 大 1000 倍
199       13s          2.3MB

特征:Max / Median 比值很大(> 3~5 倍就是倾斜信号,> 10 倍基本确认)。

SQL / DataFrame 页面

看每个算子耗时:

  • 找到最耗时的 operator
  • 看 Exchange(Shuffle)节点前后的数据量变化
  • COUNT(DISTINCT) 会展开为 groupBy + groupBy

Executor 页面

┌────────────────────────────────────────────┐
│ Executor 1: Tasks 50, GC Time 35%         │ ← GC 过高
│ Executor 2: Tasks 50, GC Time 8%          │
│ Executor 3: Tasks 3,  GC Time 2%          │ ← 调度不均
│ Executor 4: Failed Tasks 5                │ ← 有失败重试
└────────────────────────────────────────────┘

问题信号:

  • 各 Executor 任务数严重不均 → 调度问题或数据倾斜
  • 个别 Executor GC 时间极高 → 该节点数据过大
  • Failed Tasks 多 → 内存不足或数据问题

2.2 执行计划 —— EXPLAIN

EXPLAIN EXTENDED
SELECT ... FROM ... WHERE ... GROUP BY ...

读执行计划的关键是找 Exchange 节点,以及它前后的数据量变化。

常见问题模式:

计划特征 问题 解决方向
HashAggregateExchangeHashAggregate COUNT(DISTINCT) 展开了两次聚合 改用 bitmap/approx
Exchange on 大表 JOIN 小表 小表没广播 hint /*+ BROADCAST(small) */
多个连续 Exchange 多次 Shuffle 合并聚合,减少中间步骤
SortMergeJoin on 大表×大表 两张大表都需 Shuffle 考虑分桶或预聚合
BroadcastHashJoin 小表广播,最优 确认广播阈值够大

2.3 日志与 Metrics

# 找到倾斜的 partition 对应的 key
# 方法:抽样统计 key 分布
SELECT key_col, COUNT(*) AS cnt
FROM source_table
WHERE dt = '2026-05-01'
GROUP BY key_col
ORDER BY cnt DESC
LIMIT 20;

2.4 常见问题 - 定位速查表

现象 根因 判断方法
个别 Task 特别慢 数据倾斜 Stage 页看 Duration max/median
所有 Task 都慢 数据量大 / 资源不足 看 Input Size、并行度
Shuffle Write 巨大 聚合不够早 看 Exchange 前的数据量
OOM / 大量 Spill 内存不足 / key 太多 Executor 页看 GC%、Spill
Stage 间有长空闲 调度延迟 看时间线中的 gap
Task 反复重试 数据或资源问题 Executor 页 Failed Tasks

3. 通用优化策略

3.1 减少数据扫描

原则:读最少的数据,尽早过滤。

-- ❌ 先 JOIN 后过滤
SELECT ... FROM big_table a
JOIN big_table b ON a.id = b.id
WHERE a.dt = '2026-05-01';

-- ✅ 先过滤后 JOIN(谓词下推,但显式写更可靠)
WITH filtered AS (
    SELECT * FROM big_table WHERE dt = '2026-05-01'
)
SELECT ... FROM filtered a JOIN filtered b ON a.id = b.id;

关键技巧:

  • 分区表一定在 WHERE 中指定分区字段
  • 只 SELECT 需要的列(列裁剪),不要 SELECT *
  • 能提前过滤的 WHERE 条件不要放到 HAVING 或 JOIN 之后

3.2 减少 Shuffle 次数

-- ❌ 多次独立聚合,触发多次 Shuffle
,agg1 AS (SELECT brand, SUM(gmv) AS total_gmv FROM t GROUP BY brand)
,agg2 AS (SELECT brand, SUM(dot) AS total_dot FROM t GROUP BY brand)

-- ✅ 一次聚合完成(或使用窗口函数)
,agg AS (
    SELECT brand,
           SUM(gmv) AS total_gmv,
           SUM(dot) AS total_dot,
           -- 大粒度的汇总用窗口函数,不额外 Shuffle
           SUM(SUM(gmv)) OVER (PARTITION BY category) AS category_gmv
    FROM t
    GROUP BY brand, category
)

窗口函数 vs 独立聚合的选择:

场景 推荐 原因
同粒度多指标 一次 GROUP BY 一次 Shuffle
父级汇总 窗口函数 复用同一 Shuffle 结果
COUNT(DISTINCT) 汇总 独立聚合 窗口 SUM(distinct) 无意义
多层不同粒度 视情况,优先合并 每层 GROUP BY 都是一个 Shuffle

3.3 JOIN 优化

-- 1. 小表 JOIN 大表 → Broadcast Hint
SELECT /*+ BROADCAST(small_table) */ ...
FROM small_table a JOIN big_table b ON a.key = b.key;

-- 2. 大表 JOIN 大表 → 考虑分桶(Bucketing)
-- 建表时按 JOIN key 分桶,后续 JOIN 可避免 Shuffle
CREATE TABLE orders (id BIGINT, ...)
CLUSTERED BY (user_id) INTO 256 BUCKETS;

-- 3. 避免笛卡尔积
-- JOIN 条件中必须包含等值条件

JOIN 类型自动选择逻辑:

条件 自动选择的 JOIN 类型
小表 < spark.sql.autoBroadcastJoinThreshold(默认 10MB) BroadcastHashJoin
两表都大,JOIN key 可排序 SortMergeJoin
两表都大,无合适 key ShuffledHashJoin(需一方小于 spark.sql.autoBroadcastJoinThreshold × 3)

3.4 避免 COUNT(DISTINCT) 陷阱

-- ❌ 多个 COUNT(DISTINCT) — 每个都会展开为独立的 groupBy + groupBy
SELECT
    COUNT(DISTINCT col1),
    COUNT(DISTINCT col2),
    COUNT(DISTINCT col3)
FROM t;

-- ✅ 改写:使用条件聚合替代部分 COUNT(DISTINCT)
SELECT
    COUNT(DISTINCT col1),
    COUNT(DISTINCT IF(condition, col2, NULL))  -- 合并进同一个 DISTINCT
FROM t;

-- ✅ 或使用 approx_count_distinct(误差 ~2%)
SELECT approx_count_distinct(col1) FROM t;

为什么 COUNT(DISTINCT) 慢:

  1. Spark 将 COUNT(DISTINCT col) 展开为:先 (col) GROUP BY,再 COUNT(*)
  2. 多个 COUNT(DISTINCT) 不能共享 Shuffle
  3. distinct 的 key 往往膨胀(高基数),Shuffle 数据量大

3.5 替代 collect_set + array_contains 笛卡尔积

这是一个容易被忽视的性能杀手。

-- ❌ 笛卡尔积方式(常见于计算「该品类下各品牌用户数」)
WITH user_brands AS (
    SELECT user_id, COLLECT_SET(brand) AS brand_set
    FROM t GROUP BY user_id
),
all_brands AS (
    SELECT DISTINCT brand FROM t
)
SELECT b.brand, SUM(CASE WHEN ARRAY_CONTAINS(brand_set, b.brand) THEN 1 ELSE 0 END)
FROM user_brands u JOIN all_brands b  -- 笛卡尔积!
GROUP BY b.brand;

-- ✅ 等价于直接 COUNT(DISTINCT user_id) GROUP BY brand
SELECT brand, COUNT(DISTINCT user_id)
FROM t
GROUP BY brand;

本质:COLLECT_SET + ARRAY_CONTAINS 的 INNER JOIN 就是 COUNT(DISTINCT) GROUP BY。 改写后从 O(n×m) 降到 O(n),且避免了 collect_set 的 OOM 风险。


4. 数据倾斜:识别与处理

4.1 什么是数据倾斜

Spark 按 key 的 hash 值将数据分配到不同分区。当某些 key 的数据量远大于其他 key 时:

Partition 0: ████████████████████████████████  200MB  (Task 耗时 15min)
Partition 1: ██                                   5MB  (Task 耗时 20s)
Partition 2: ██                                   6MB  (Task 耗时 22s)
Partition 3: ███                                  8MB  (Task 耗时 30s)
...
Partition 199: ████                              10MB  (Task 耗时 35s)

倾斜的 partition 所在的 Task(有时称为「长尾任务」)拖慢整个 Stage。

4.2 倾斜的根因

原因 典型场景 例子
热点 key 少量 key 有海量数据 brandname_full = '全部' 作为汇总行
NULL 值聚集 JOIN 或 GROUP BY key 大量为 NULL 未关联上的数据全落到同一分区
数据分布天然不均 业务规律 头部品牌占 80% 交易,尾部品牌数据极少
JOIN key 基数极低 只有几个值 data_type = '全部'item_cate = '全部'

4.3 解决方案矩阵

按场景选择合适方案:

方案 1:过滤倾斜 key(适用:倾斜 key 对业务无意义)

-- 如果 NULL 或 '全部' 不需要参与计算,直接过滤
WHERE brandname_full <> '全部'
  AND user_id IS NOT NULL

方案 2:两阶段聚合(适用:GROUP BY 场景的倾斜)

-- ❌ 直接聚合,hot key 全部进一个 Task
SELECT key, SUM(val) FROM t GROUP BY key;

-- ✅ 两阶段聚合:加随机前缀 → 预聚合 → 去前缀再聚合
WITH salted AS (
    SELECT CONCAT(key, '_', CAST(RAND() * 100 AS INT)) AS salted_key, val
    FROM t
),
pre_agg AS (
    SELECT salted_key, SUM(val) AS partial_sum
    FROM salted
    GROUP BY salted_key
)
SELECT
    SUBSTRING_INDEX(salted_key, '_', 1) AS key,
    SUM(partial_sum) AS total
FROM pre_agg
GROUP BY SUBSTRING_INDEX(salted_key, '_', 1);

原理: 第一层聚合将 hot key 打散到 100 个分区各自局部聚合,第二层聚合的数据量已经缩小 100 倍。

适用条件: 聚合函数可拆分(SUM、COUNT 可以;AVG 需要拆成 SUM/COUNT 两个阶段;COUNT(DISTINCT) 不能直接用此方案)。

方案 3:倾斜侧加盐(适用:JOIN 场景,一侧有倾斜)

-- ❌ 直接 JOIN,hot key 集中在一个 Task
SELECT a.*, b.val
FROM big_table a
JOIN lookup_table b ON a.key = b.key;

-- ✅ 大表加随机盐,小表复制 N 份
WITH big_salted AS (
    SELECT
        CONCAT(key, '_', CAST(RAND() * 10 AS INT)) AS salted_key,
        *,
        CAST(RAND() * 10 AS INT) AS salt
    FROM big_table
),
small_expanded AS (
    SELECT
        CONCAT(key, '_', s.salt) AS salted_key,
        l.*
    FROM lookup_table l
    CROSS JOIN (SELECT EXPLODE(ARRAY(0,1,2,3,4,5,6,7,8,9)) AS salt) s
)
SELECT a.*, b.val
FROM big_salted a
JOIN small_expanded b ON a.salted_key = b.salted_key;

适用条件: 一侧表较小(可接受 N 倍膨胀),另一侧有大 key 倾斜。

方案 4:广播 JOIN(适用:小表 JOIN 大表)

SELECT /*+ BROADCAST(small_table) */ ...
FROM big_table JOIN small_table ON big_table.key = small_table.key;

广播 JOIN 没有 Shuffle,从根本上避免了倾斜。调整阈值:

SET spark.sql.autoBroadcastJoinThreshold = 104857600;  -- 100MB

方案 5:分桶(Bucketing)(适用:高频 JOIN 的大表×大表)

-- 建表时定义分桶
CREATE TABLE orders
CLUSTERED BY (user_id) INTO 256 BUCKETS;

CREATE TABLE users
CLUSTERED BY (user_id) INTO 256 BUCKETS;

-- JOIN 时同 key 同桶数,无需 Shuffle
SELECT /*+ BROADCAST(a) */ ... FROM orders a JOIN users b ON a.user_id = b.user_id;

注意: 分桶只在两表桶数相同(或成倍数)且 JOIN key = 分桶 key 时才生效。写入时需保证 spark.sql.shuffle.partitions 与桶数一致。

方案 6:分离处理(适用:倾斜 key 是少量的特定值)

-- 把倾斜 key 单独处理,非倾斜 key 正常处理
WITH
skewed_data AS (
    SELECT * FROM t WHERE key IN ('hot_key_1', 'hot_key_2')
),
normal_data AS (
    SELECT * FROM t WHERE key NOT IN ('hot_key_1', 'hot_key_2')
),
-- 倾斜数据用加盐处理
skewed_agg AS ( /* 方案 2 两阶段聚合 */ ),
-- 正常数据直接聚合
normal_agg AS ( /* 直接 GROUP BY */ )
SELECT * FROM skewed_agg UNION ALL SELECT * FROM normal_agg;

方案 7:调整分区策略(适用:key 分布不均但无极端热点)

-- 默认 Hash Partitioning → 改为 Range Partitioning
SELECT /*+ REPARTITION(200, key_col) */ ...;

-- 或使用 DISTRIBUTE BY 控制分区
SELECT ... FROM t DISTRIBUTE BY key_col;

方案 8:开启 AQE(Adaptive Query Execution)

Spark 3.0+ 的 AQE 可以在运行时自动处理部分倾斜:

SET spark.sql.adaptive.enabled = true;
SET spark.sql.adaptive.skewJoin.enabled = true;
SET spark.sql.adaptive.skewJoin.skewedPartitionFactor = 5;
SET spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes = 256MB;
SET spark.sql.adaptive.coalescePartitions.enabled = true;

AQE 能自动做什么:

  • 运行时合并小分区(减少 Task 数量)
  • 检测并拆分倾斜的分区
  • 动态调整 JOIN 策略

但不能完全依赖 AQE: 极端倾斜、COUNT(DISTINCT) 场景、自定义 UDAF 等场景下 AQE 效果有限,仍需要前面的人工优化手段。

4.4 方案选择决策树

遇到数据倾斜
├─ 倾斜 key 可以过滤掉?
│  └─ Yes → 方案 1: 直接过滤(最简单)
│
├─ 是 JOIN 操作?
│  ├─ 小表 < 100MB?
│  │  └─ Yes → 方案 4: 广播 JOIN
│  │
│  ├─ 倾斜只在少数 key?
│  │  └─ Yes → 方案 6: 分离处理
│  │
│  └─ 大表 × 大表?
│     ├─ 频繁 JOIN → 方案 5: 分桶
│     └─ 临时查询 → 方案 3: 加盐 JOIN
│
├─ 是 GROUP BY?
│  └─ 方案 2: 两阶段聚合
│
└─ Spark 3.0+
   └─ 开启 AQE → 方案 8: 作为兜底

5. 配置调优指南

5.1 常见配置误区及修正

参数 常见误区 建议值 原因
spark.sql.shuffle.partitions 设 2000-3000 200-800 分区太多 → Task 调度开销大、小文件多
mapred.max.split.size 16MB 128MB-256MB 切片太小 → Task 太多、每个 Task 处理的元数据开销占比大
spark.dynamicAllocation.maxExecutors 1000 200-400 过多 Executor 增加调度延迟和资源争抢
spark.executor.cores 8-16 4-8 核数过多 → 并发 I/O 竞争、GC 压力
spark.executor.memory 4-8G 8-16G 视数据量而定,过大浪费
spark.sql.autoBroadcastJoinThreshold 10MB(默认) 50MB-100MB 提高广播阈值,减少 Shuffle JOIN
spark.sql.adaptive.enabled false(Spark 3.0 前) true AQE 自动优化

5.2 根据数据量估算并行度

总输入数据量 / 128MB ≈ 分区数下限
总输入数据量 / 256MB ≈ 分区数上限

例如:输入 100GB 数据
  100GB / 128MB ≈ 800 分区(上限)
  100GB / 256MB ≈ 400 分区(下限)

  推荐: spark.sql.shuffle.partitions = 400~600

每个 Task 处理 128MB ~ 256MB 数据比较理想。 太小 → 调度开销大,太大 → GC 压力大。

5.3 内存分配

单个 Executor 内存分配原则:

  spark.executor.memory = 16G
  spark.executor.memoryOverhead = max(384MB, 0.1 × 16G) = 1.6G
  spark.executor.cores = 4

  每个 Task 可用内存 = (16G - storage memory) / 4 ≈ 3~4G

如果 Spill 严重:

  • 增大 spark.executor.memory
  • 或减小 spark.executor.cores(减少并发,增加单 Task 内存)

5.4 生产配置模板

-- Spark 3.x 生产环境推荐配置
SET spark.sql.adaptive.enabled = true;
SET spark.sql.adaptive.coalescePartitions.enabled = true;
SET spark.sql.adaptive.skewJoin.enabled = true;
SET spark.sql.adaptive.skewJoin.skewedPartitionFactor = 5;
SET spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes = 256MB;

SET spark.sql.shuffle.partitions = 400;
SET spark.sql.autoBroadcastJoinThreshold = 104857600;    -- 100MB
SET spark.sql.hive.mergeFiles = true;                     -- 合并小文件
SET spark.sql.parser.quotedRegexColumnNames = false;      -- 避免正则列名歧义

SET spark.dynamicAllocation.maxExecutors = 400;
SET spark.dynamicAllocation.minExecutors = 20;
SET spark.executor.cores = 4;
SET spark.executor.memory = 16G;
SET spark.driver.memory = 16G;

6. 实战案例复盘

以「药品品牌竞品分析 SQL」为例,演示一个完整的优化过程。

6.1 原始 SQL 的问题

-- 原始代码结构
WITH ord1 AS (SELECT ... FROM source WHERE ...),          -- 扫描一次
ord1_stats AS (SELECT ... FROM ord1 GROUP BY ...),        -- 聚合 1,Shuffle 1
ord1_stats_without_brand AS (... FROM ord1 GROUP BY ...), -- 聚合 2,Shuffle 2 (重复扫 ord1)
ord1_usr_brand_set AS (SELECT collect_set(...) ...),      -- 聚合 3,Shuffle 3
ord1_all_brands AS (...),                                 -- 聚合 4,Shuffle 4
ord1_usr_brand_set_fg AS (collect_set + WHERE ...),       -- 聚合 5,Shuffle 5
ord1_all_brands_fg AS (...),                              -- 聚合 6,Shuffle 6
ord1_usr_stats AS (JOIN + array_contains ...),            -- 笛卡尔积!
ord1_fg_usr_stats AS (同上),                              -- 又一个笛卡尔积!
ord1_final AS (JOIN × 4),                                -- 4 个 LEFT JOIN

-- ord2 完全重复上面整套逻辑

问题清单:

问题 严重程度 影响
同一张源表扫了 2 次(ord1_stats + ord1_stats_without_brand 各自扫 ord1) 双倍扫描 I/O
collect_set + array_contains 笛卡尔积计算用户数 致命 O(n×m) 复杂度,大量 Shuffle
ord1 整套逻辑在 ord2 完全重复 双倍计算资源
8 个 CTE 各触发独立聚合 至少 6 次 Shuffle
spark.sql.shuffle.partitions = 3000 3000 个小分区,调度开销大

6.2 优化后的结构

WITH
ord1_base AS (SELECT ... FROM source WHERE ...),         -- 扫描 1 次

ord1_stats AS (                                          -- 一次聚合完成品牌+品类
    SELECT brand, ...,
        SUM(SUM(gmv))  OVER (PARTITION BY drug) total_gmv,  -- 窗口函数替代独立聚合
        SUM(SUM(dot))  OVER (PARTITION BY drug) total_dot,
        ...
    FROM ord1_base
    GROUP BY brand, drug, category
),

single_brand_users AS (                                  -- 找出只买一个品牌的用户
    SELECT user, drug, MAX(brand) sole_brand,
           MAX(parent_ord_cnt) max_ord
    FROM ord1_base
    GROUP BY user, drug
    HAVING COUNT(DISTINCT brand) = 1
),

brand_exclusive_users AS (                               -- 每个品牌的独占用户数
    SELECT drug, sole_brand brand,
           COUNT(DISTINCT user) exclusive_cnt
    FROM single_brand_users
    GROUP BY drug, sole_brand
),

ord1_final AS (                                          -- 一次 JOIN 产出所有指标
    SELECT ...
    FROM ord1_stats s
    LEFT JOIN drug_level_users d ON ...
    LEFT JOIN brand_exclusive_users e ON ...
)

-- ord2 同上(复用结构)

6.3 优化效果对比

维度 优化前 优化后 提升
Shuffle 次数 ~12 次 ~4 次 -67%
源表扫描 2 次 1 次 -50%
CTE 数量 20 个 12 个 更清晰的 DAG
笛卡尔积 2 处 0 处 消除
竞品用户数口径 bug(= 本品用户数) 修复 正确
shuffle.partitions 3000 400 调度效率 +7x

6.4 还有一个容易忽略的点:collect_set 的 OOM 风险

-- ❌ 如果某个用户的 brand_set 包含数百个品牌:
--    collect_set 的单个 key 巨大 → OOM
collect_set(brandname_full) AS brand_set

-- ✅ 等价改写为 COUNT(DISTINCT) 后,不再有 collect 操作
--    Spark 内部用哈希聚合,数据分散在各节点内存中
COUNT(DISTINCT user_log_acct) GROUP BY brandname_full

7. 优化检查清单

写 SQL 或 Review SQL 时,逐项检查:

数据读取

Shuffle 与 聚合

JOIN

倾斜

配置

验证


附:快速参考卡片

┌────────────────────────────────────────────────────────────┐
│                 Spark SQL 优化速查                          │
├────────────────────────────────────────────────────────────┤
│  Shuffle 次数:    越少越好                                  │
│  分区数:         400~800(每 Task 128-256MB)               │
│  Broadcast:      小表 < 100MB 就 broadcast                 │
│  倾斜信号:       max/median Duration > 5x                  │
│  倾斜处理:       过滤 → 两阶段聚合 → 加盐 → AQE             │
│  COUNT DISTINCT: 多个同字段合并,或用 approx               │
│  窗口函数:       同粒度指标用窗口函数避免二次聚合             │
│  定位入口:       Spark UI → Stage → Summary Metrics        │
└────────────────────────────────────────────────────────────┘

优化不在一蹴而就,而在于每次写 SQL 时多问一句:「这一步会 Shuffle 吗?能省吗?」

posted on 2026-05-18 11:07  茶倌  阅读(12)  评论(0)    收藏  举报