订阅更新

一个 SQL 跑了 3 小时——你会从哪里开始排查?

数仓开发手册 第 5 题 约 11 分钟读完

先说结论

SQL 慢排查四步走:(1)看执行计划找瓶颈算子、(2)看数据分布找倾斜、(3)看 IO 和 Shuffle 量、(4)针对性优化。这四步是标准动作,面试时按这个顺序讲。

最常见的三大病因:数据倾斜、全表扫描、笛卡尔积。80% 的慢 SQL 都是这三个。

优化手段要分层:SQL 改写(零成本)> 引擎参数(低成本)> 资源扩容(高成本)。先试零成本的,最后才上资源。

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

详细讲解

『这个 SQL 跑了 3 小时还没出结果,你会怎么排查』——这是 SQL 实操阶段的第二道必问题。不是考语法,是考你有没有在线上扛过慢 SQL。新人的答案是『加个 LIMIT 试试』或『建个索引』,老手的答案是一套排查流程:先看执行计划、再看数据分布、最后动手改 SQL 或加参数。这题面试官想听到的就是这个『流程感』。

业务场景:某出海硬件品牌的 ETL 主作业 3 小时变 30 分钟

我在某出海硬件品牌做 Databricks 平台时,有一条核心 ETL 主作业从早先的 3 小时慢慢长到了 3.5 小时,凌晨跑完时间越过了早上 8 点看板的窗口。老板让我优化到 1 小时内。

排查下来是三个问题叠加:一个 SELECT * 没做列裁剪(Shuffle 数据量是真实需要的 3 倍)、一个设备 ID 是内部测试设备占了 30% 的数据(数据倾斜)、一个维度表 80MB 没触发 Broadcast Join(走了 SortMergeJoin)。三个问题各自修完,执行时间从 3.5 小时降到 28 分钟。

-- 优化前:3.5 小时
SELECT *
FROM fact_device_event e
JOIN dim_device d ON e.device_id = d.device_id  -- 80MB 维表走 SMJ
WHERE e.dt = '2024-03-15';                        -- 有一个测试设备占 30%
 
-- 优化后:28 分钟
SELECT /*+ BROADCAST(d) */
  e.dt, e.device_id, e.event_name, e.event_ts, d.country, d.model
FROM fact_device_event e
JOIN dim_device d ON e.device_id = d.device_id
WHERE e.dt = '2024-03-15'
  AND e.device_id != 'TEST_DEVICE_XXX';
 
-- 配合参数
SET spark.sql.adaptive.enabled = true;              -- 开 AQE
SET spark.sql.autoBroadcastJoinThreshold = 200MB;   -- 10MB 调到 200MB

段落一:第一步——看执行计划

慢 SQL 排查的第一步永远是看执行计划。EXPLAIN 不会骗人,你以为你写了什么、优化器实际怎么执行,差距经常很大。重点看四件事:有没有全表扫描、Join 是什么类型(Broadcast/SortMerge/Shuffle Hash)、Shuffle 有几次、有没有奇怪的 Sort。

-- Spark / Hive / Presto 都支持
EXPLAIN FORMATTED
SELECT ... FROM ...;
 
-- Spark 更详细的版本(看 AQE 改写后的 plan)
EXPLAIN EXTENDED
SELECT ... FROM ...;
 
-- 执行完后看实际用时
-- Spark UI / MaxCompute LogView / Hive counters
-- 每个 stage 的 task 数、数据量、最慢 task 用时

关键阅读要点:看 Exchange(Shuffle)算子有几个——一般一个 JOIN 就是一次 Shuffle,三个 JOIN 就是三次 Shuffle,Shuffle 是成本最高的算子;看 Join 类型——SortMergeJoin 慢、BroadcastHashJoin 快;看 Scan 有没有 PushedFilters——没有就是没有过滤下推。

段落二:第二步——看数据分布找倾斜

如果执行计划看起来正常但还是慢,80% 是数据倾斜。倾斜的典型表现:99% 的 task 几分钟跑完,剩下 1% 跑了 40 分钟。Spark UI 上看 Task 的 Duration 分布,中位数和 max 差 10 倍以上基本就是倾斜。

-- 诊断数据倾斜:看 join key 的分布
SELECT device_id, COUNT(*) AS cnt
FROM fact_device_event
WHERE dt = '2024-03-15'
GROUP BY device_id
ORDER BY cnt DESC LIMIT 10;
 
-- 如果发现某个 key 占比 > 10%,就是倾斜了
-- 典型的倾斜 key:
-- - 测试账号 / 测试设备(内部 QA 天天跑测试)
-- - NULL / 空字符串(各种脏数据都归到 NULL)
-- - 默认值(默认地域、默认渠道)

我在某出海硬件品牌的真实案例:有一个测试设备 device_id 占了 30% 的数据,Join 时所有这个 key 都 Shuffle 到同一个 reducer,40 分钟跑不完。解决办法是开启 AQE(Adaptive Query Execution)的 skew join 自动处理,执行时间从 40 分钟降到 8 分钟。也可以手动加盐(给 key 加随机前缀)但代价更大。

段落三:第三步——看 IO 和 Shuffle 量

执行计划对了、数据不倾斜,但还慢,那就要看 IO。最常见的 IO 问题是 SELECT * 导致的列冗余。

-- ❌ SELECT * 的代价
-- 原表有 50 个字段,真正用到的只有 5 个
-- Shuffle 时 50 个字段全部序列化走网络
SELECT *
FROM fact_event JOIN dim_user USING (uid);
 
-- ✅ 列裁剪
SELECT e.dt, e.event_name, e.event_ts, u.country, u.level
FROM fact_event e JOIN dim_user u USING (uid);
 
-- 我在某出海硬件品牌的真实收益:
-- SELECT * 改列裁剪后 Shuffle 量减少 30%-45%
-- 执行时间相应下降

另一个 IO 杀手:没做分区过滤。一张 10 亿行的分区表,WHERE 里忘写 dt 过滤条件,直接全表扫。这种错误在 code review 时要被 flag,上线前一定要 EXPLAIN 确认 Scan 的 PartitionFilters 里有你期望的分区过滤条件。

段落四:第四步——针对性优化

前三步定位了问题,第四步根据问题类型选择优化方案。

问题类型优化手段预期收益
数据倾斜AQE skew join / 加盐 / 倾斜 key 单独处理40 分钟→8 分钟(我的真实数据)
Broadcast 没触发调大 autoBroadcastJoinThreshold / 加 /*+ BROADCAST */ hint作业时间减少 40%(我的真实数据)
SELECT * 冗余 IO只 SELECT 需要的列Shuffle 量减少 30%-45%
Join 多次 Shuffle预聚合到 DWS 层 / 调 shuffle 分区数整体 ETL 时间减少 40%
UDF 性能差改原生 SQL / 用 Codegencase-by-case
小文件过多coalesce / 合并小文件Scan 时间减少 50%+

面试高频追问

追问一:autoBroadcastJoinThreshold 调多大合适?调太大会怎么样?

默认 10MB 太小了,现在集群内存都很大,我在某出海硬件品牌调到 200MB,大部分维度表都能 Broadcast,Join 自动变 Broadcast,作业时间减少 40%。调太大的风险是 Driver OOM——因为 Broadcast 表会先拉到 Driver 再广播出去,Driver 内存撑不住。经验值:Driver 内存 4G 调到 200MB 是安全的,再大要把 Driver 内存也调大。

追问二:AQE 除了 skew join 还能做什么?

AQE 在运行时根据实际的数据量重新决策,三大能力:(1)自动合并 shuffle 分区——如果 shuffle 后很多分区很小自动合并,减少 task 数;(2)自动处理 skew join——检测到倾斜自动拆分;(3)自动切换 join 策略——SortMergeJoin 跑一半发现有一边变小了自动切成 BroadcastHashJoin。Spark 3.x 之后默认开启,是零成本收益。

追问三:如果以上都试了还是慢,你会怎么办?

回到业务逻辑层面:是不是这个 SQL 本身就该拆?200 行一条 SQL 里套 6 个 CTE 的场景,拆成几个中间表(落盘成临时 DWS)往往比怎么调参数都快,因为你人为帮优化器做了决策。另一种是时间换空间:预聚合到 DWS 层,ADS 层只做最后拼接。第 14 篇 SQL 1200 行那个故事就是用这种方法从 24 小时跑不完变成 8 分钟。

实战踩坑:我从 RDD 改 DataFrame 性能翻倍的一次

我在某出海硬件品牌接手过一个老的 Spark 作业,原作者用 RDD API 写的,跑 2 小时。我改写成 DataFrame 之后同样逻辑跑 1 小时——执行时间减少 50%。这个差距的本质是 Catalyst 优化器:DataFrame 能被 Catalyst 优化,RDD 不能。

教训:面试时如果被问到 DataFrame 和 RDD 的区别,要能讲出这个真实差距。不是『DataFrame 有 schema、RDD 没有』这种教科书答案,而是『DataFrame 可以被 Catalyst 优化器重写,RDD 是黑盒,优化器看不见』。

// ❌ RDD API:优化器看不见
val result = rdd1.map(...).filter(...)
  .keyBy(_.uid).join(rdd2.keyBy(_.uid))
  .map{case (k, (a, b)) => ...}
 
// ✅ DataFrame API:Catalyst 会重写
val result = df1.join(df2, 'uid)
  .filter(...)
  .select(...)
// Catalyst 会自动:谓词下推、列裁剪、Join 重排序

总结

SQL 慢排查的关键是『流程感』而不是『招数』。面试时按『看执行计划 → 看数据分布 → 看 IO → 针对性优化』四步走,每一步讲一个真实场景和数字。能把 AQE、Broadcast Join、列裁剪这三个手段说清楚带上真实收益(40 分钟→8 分钟、减少 40%、减少 30%-45%),这道题就达到了资深的水平。

下一篇:『数据倾斜怎么解决?』——这是慢 SQL 排查里最常见的一类,面试官会单独再挖一次。加盐、两阶段聚合、AQE、倾斜 key 单独处理——这些手段的适用场景完全不一样。

你排查过的最慢的一条 SQL 是多长时间?最后是怎么优化的?是数据倾斜还是别的原因?

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

数仓开发 面试通关手册

第6-10篇:数据倾斜 + 建模深挖(面试 20-35 分钟)

Wood聊大数据

9年大数据老兵,聊技术、聊职场、聊成长

按面试官真实提问顺序设计的 20 道题

本部分导读(面试 20-35 分钟:建模深挖阶段)

面试进行到 20 分钟以后,如果你前面暖场和 SQL 实操讲得还可以,面试官通常会开始深挖你的建模功底。这就是本册要讲的第 6-10 篇。

第 6 篇是 SQL 实操的尾声——数据倾斜。这是上一本册慢 SQL 排查的延伸,面试官会单独再挖一次,考察你对四种解决方案(AQE、两阶段聚合、加盐、倾斜 key 单独处理)的边界判断。第 7-10 篇就是完整的建模四连问:维度表设计、宽表设计、拉链表、分区字段。这四题下来,基本能判断你的建模是科班水平还是野路子。

建模题的特点是『没有标准答案、全是 trade-off』,面试官更看你的判断过程。本册每一题都会给出多种方案对比 + 选型理由 + 真实返工故事。

— Wood,2026 年春

刷题进度保存在当前浏览器