外观
HiveSQL 慢查询怎么排查
难度与前置
难度:中级
前置:会跑 EXPLAIN;知道分区表和 Join 基本概念。
读完能做什么:把「慢」拆到排队/扫描/Shuffle/输出,按清单改 SQL,并用扫字节与结果集验收。
适用场景
- 日报 / 主题 SQL 墙钟突然变长,或经常队列超时
- 同一逻辑换一天数据就慢一个数量级
- 有人说「加内存 / 加并行」试了很多轮仍不稳定
不适用:
- 还没确认是本 SQL 慢,还是上游晚、队列堵(先看调度时间线)
- 引擎已是 Spark SQL / Impala 等:排查思路可借鉴,参数名以引擎为准
真实场景(一条)
投放主题 DWS 任务:渠道 × 剧本 × 天 汇总,平时 12 分钟,某日跑到 90 分钟。
先拆耗时发现:YARN 排队只多了 3 分钟,其余大多在某个 Join Stage 长尾。EXPLAIN 显示分区条件写在子查询外层,扫了近 30 天 ODS。把 dt 谓词下推进子查询后,扫字节降一个数量级,墙钟回到约 15 分钟。
结论:先定位贵在哪,再改 SQL;别一上来调参。
怎么做(步骤清单)
1. 先拆四段耗时(别急着加资源)
| 阶段 | 看什么 | 常见根因 |
|---|---|---|
| 排队与启动 | 提交到首个 Task 的间隔 | 队列满、并发限制、上游晚 |
| 读取与过滤 | 扫分区数、文件数、字节数 | 分区未裁剪、列过多、小文件 |
| Join / 聚合 / Shuffle | Stage 读写字节、长尾 Task | 大表 Shuffle Join、倾斜、Join 爆炸 |
| 落盘与输出 | 写出文件数、动态分区数 | 小文件、动态分区过多、目标存储慢 |
日志 / UI 里把墙钟对上这四段;哪段占比最高就先攻哪段。
2. 用 EXPLAIN 确认数据怎么流
sql
EXPLAIN
SELECT ...
FROM ...
WHERE dt = '${biz_date}';重点确认:
- 分区过滤是否下推到读表
- Join 两侧输入规模、是否广播 / Shuffle
- 有没有多余全局排序、重复
DISTINCT、多次 Shuffle - 聚合在数据放大前还是放大后
执行计划是为了回答:读了多少、在哪变多、为什么必须搬数据。
若统计信息过期,优化器可能选错 Join 顺序——顺带检查 ANALYZE / 表统计是否新鲜。
3. 数据侧 Checklist(扫描与倾斜)
- [ ]
WHERE dt = ...是否真在扫单分区(对上 分区策略 的EXPLAIN验收) - [ ] 是否
SELECT *拖进大宽列 - [ ] Join 键是否倾斜(少数 key 占比过高)
- [ ] Join 前后行数是否暴涨(粒度没对齐,参考 分层文 粒度契约)
- [ ] 源路径小文件是否过多(Task 数爆、启动开销大)
4. SQL 改写优先序(有依据再动)
按收益常见顺序:
- 谓词下推 / 分区条件贴近读表
- 先过滤、再 Join;小维表可 MapJoin / 广播(确认体量上限)
- 对齐粒度后再关联,避免头表 × 明细爆炸
- 能预聚合就进子查询先
GROUP BY,减少 Shuffle 体积 - 避免无必要
ORDER BY、重复去重;DISTINCT前先问「唯一键是什么」 - 最后才调并行度、内存、合并小文件等参数
示意:把过滤推进去:
sql
-- 弱:外层才过滤,子查询可能先做大 Join
SELECT ...
FROM (
SELECT o.*, c.settle_entity
FROM dwd_ad_spend_detail_di o
JOIN dim_ad_channel_zipper c
ON o.channel_id = c.channel_id
) t
WHERE t.dt = '${biz_date}';
-- 强:读表即裁剪,再按业务日贴维表区间
SELECT o.channel_id, c.settle_entity, SUM(o.cost_amount) AS cost_amount
FROM dwd_ad_spend_detail_di o
JOIN dim_ad_channel_zipper c
ON o.channel_id = c.channel_id
AND c.valid_from <= o.dt
AND o.dt < c.valid_to
WHERE o.dt = '${biz_date}'
AND o.is_valid = true
GROUP BY o.channel_id, c.settle_entity;5. 引擎参数:当「确认瓶颈之后」的加速器
在已经定位阶段后再动,例如:
- 扫描慢且小文件多 → 合并输入 / 控制输出文件数
- 广播维表 → 提高 mapjoin 阈值前先量维表体积
- Shuffle 内存溢出 → 适度加大内存或减单 Task 数据量(先解决倾斜更重要)
WARNING
没有基线指标就改参数 = 配置试错。每次只改一类旋钮,并留下扫字节与耗时对比。
6. 固定排查记录模板(方便复盘)
text
作业:xxx / biz_date:
墙钟:xx → 四段占比:排队/扫描/Shuffle/输出 =
EXPLAIN 结论:
改动:
扫字节:改前 → 改后
结果校验:通过 / 差异说明常见坑
没拆耗时就加资源
排队慢加 Executor 没用;扫描慢加内存也没用。分区条件写在最外层
子查询已全表 Join,外层dt救不回来。隐式类型转换导致裁剪/Join 失效
dt与常量类型不一致、键一边STRING一边BIGINT,计划看起来「有过滤」实际仍大扫。用
SELECT *+ 大维表广播
广播的是整行宽表,内存直接打爆;只选需要列。把倾斜当成「再加并行」
热点 key 不会因并行变均匀;要单独打散、两阶段聚合或拆热点。
如何验收(可量化)
- [ ] 四段耗时有记录,改动对准占比最高的那段
- [ ]
EXPLAIN显示目标表按dt裁剪(只扫目标分区路径) - [ ] 扫字节或读行数较改前明显下降(记下前后数字)
- [ ] 同
biz_date核心指标与改前结果集一致(或差异可解释) - [ ] 墙钟进入约定 SLA;连续两日复跑稳定
sql
-- 结果一致性示例:改写前后对同一业务日汇总对齐
-- (把 old_result / new_result 换成你的两版逻辑)
SELECT
COALESCE(o.channel_id, n.channel_id) AS channel_id,
o.cost_old,
n.cost_new,
o.cost_old - n.cost_new AS diff
FROM old_result o
FULL OUTER JOIN new_result n
ON o.channel_id = n.channel_id
WHERE COALESCE(o.cost_old, 0) <> COALESCE(n.cost_new, 0);相关面试题
题库小程序链接待定;先用站内锚点占位。
- [ ] 题:Hive SQL 慢,为什么先拆四段耗时再优化?(考察点:四段耗时记录,对准占比最高段)→ 占位:
#todo-interview-slow-sql-four-phases - [ ] 题:怎样用 EXPLAIN 证明按 dt 裁剪?(考察点:只扫目标分区)→ 占位:
#todo-interview-slow-sql-partition-prune - [ ] 题:改写后如何证明结果没 silently 变坏?(考察点:同 biz_date 核心指标一致或差异可解释)→ 占位:
#todo-interview-slow-sql-result-parity - [ ] 题:墙钟进 SLA 还要看什么才算稳定?(考察点:连续两日复跑稳定 + 扫字节下降有前后数字)→ 占位:
#todo-interview-slow-sql-sla-stable
改写说明:诊断路径参考 Wood 原创面试文,已改写成可执行清单;未包含未公开公司数据与内部集群参数。