Skip to content

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 / 聚合 / ShuffleStage 读写字节、长尾 Task大表 Shuffle Join、倾斜、Join 爆炸
落盘与输出写出文件数、动态分区数小文件、动态分区过多、目标存储慢

日志 / UI 里把墙钟对上这四段;哪段占比最高就先攻哪段。

2. 用 EXPLAIN 确认数据怎么流 ​

sql
EXPLAIN
SELECT ...
FROM ...
WHERE dt = '${biz_date}';

重点确认:

  1. 分区过滤是否下推到读表
  2. Join 两侧输入规模、是否广播 / Shuffle
  3. 有没有多余全局排序、重复 DISTINCT、多次 Shuffle
  4. 聚合在数据放大前还是放大后

执行计划是为了回答:读了多少、在哪变多、为什么必须搬数据。
若统计信息过期,优化器可能选错 Join 顺序——顺带检查 ANALYZE / 表统计是否新鲜。

3. 数据侧 Checklist(扫描与倾斜) ​

  • [ ] WHERE dt = ... 是否真在扫单分区(对上 分区策略 的 EXPLAIN 验收)
  • [ ] 是否 SELECT * 拖进大宽列
  • [ ] Join 键是否倾斜(少数 key 占比过高)
  • [ ] Join 前后行数是否暴涨(粒度没对齐,参考 分层文 粒度契约)
  • [ ] 源路径小文件是否过多(Task 数爆、启动开销大)

4. SQL 改写优先序(有依据再动) ​

按收益常见顺序:

  1. 谓词下推 / 分区条件贴近读表
  2. 先过滤、再 Join;小维表可 MapJoin / 广播(确认体量上限)
  3. 对齐粒度后再关联,避免头表 × 明细爆炸
  4. 能预聚合就进子查询先 GROUP BY,减少 Shuffle 体积
  5. 避免无必要 ORDER BY、重复去重;DISTINCT 前先问「唯一键是什么」
  6. 最后才调并行度、内存、合并小文件等参数

示意:把过滤推进去:

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 结论:
改动:
扫字节:改前 → 改后
结果校验:通过 / 差异说明

常见坑 ​

  1. 没拆耗时就加资源
    排队慢加 Executor 没用;扫描慢加内存也没用。

  2. 分区条件写在最外层
    子查询已全表 Join,外层 dt 救不回来。

  3. 隐式类型转换导致裁剪/Join 失效
    dt 与常量类型不一致、键一边 STRING 一边 BIGINT,计划看起来「有过滤」实际仍大扫。

  4. 用 SELECT * + 大维表广播
    广播的是整行宽表,内存直接打爆;只选需要列。

  5. 把倾斜当成「再加并行」
    热点 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 原创面试文,已改写成可执行清单;未包含未公开公司数据与内部集群参数。