订阅更新

面试官追问「你能现场写一个处理倾斜的SQL吗」——手写代码题怎么过?

数据倾斜手册 第 16 题 约 13 分钟读完 1 次阅读

本文目录
  1. 先说结论
  2. 手写题一:COUNT(DISTINCT)倾斜改写
  3. 手写题二:GROUP BY热点key两阶段聚合
  4. 手写题三:JOIN倾斜处理(综合版)
  5. 现场手写的答题策略
  6. 面试高频追问
  7. 实战:我准备面试时打磨手写题的方法
  8. 总结

先说结论

三类最高频手写题:COUNT(DISTINCT)倾斜改写(拆子查询)、GROUP BY热点key两阶段聚合(加盐打散)、JOIN倾斜处理(Broadcast Hint + 过滤NULL)。这三类覆盖了90%的现场手写考题。

手写时的注意事项:先说思路再写SQL(让面试官知道你在想什么,哪怕SQL有小错误,思路对也能拿分);写完后主动说出潜在问题(比如加盐后COUNT(DISTINCT)的错误);代码写整洁,注释写在关键步骤。

面试官评分重点:不只看SQL对不对,更看你能不能说出「为什么这样写」和「这种写法有什么局限」——这是区分3年和5年经验的分水岭。

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

详细讲解

大数据面试越来越喜欢考现场手写SQL,数据倾斜的手写题是高频场景。面试官给你一个倾斜场景,让你当场写出优化后的SQL。很多人平时会调优,但面试时一紧张,两阶段聚合的子查询写错、加盐的RAND()忘了怎么写,直接掉分。这篇把最高频的三类手写题完整梳理一遍,每个配上可以直接上手的模板,面试前默写两遍,上场不慌。

手写题一:COUNT(DISTINCT)倾斜改写

这是最容易考的题,也是最多人写错的题。给定场景:统计每天每个渠道的活跃用户数(DAU),原始SQL有倾斜,请改写优化。

-- 题目:改写下面有倾斜的SQL
-- SELECT dt, channel_id, COUNT(DISTINCT user_id) AS dau
-- FROM user_events
-- GROUP BY dt, channel_id;
 
-- 【错误改法】——很多人这样写,但是WRONG!
-- 不能给user_id加盐再COUNT(DISTINCT):
-- SELECT dt, channel_id, COUNT(DISTINCT CONCAT(CAST(FLOOR(RAND()*10) AS STRING),'_',user_id)) AS dau
-- FROM user_events GROUP BY dt, channel_id;
-- 原因:加了前缀后,同一个user_id变成了10个不同的user_id,计数偏大10倍!
 
-- 【正确改法一】:子查询先去重,外层COUNT(精确)
SELECT
  dt,
  channel_id,
  COUNT(user_id) AS dau  -- 内层已去重,外层COUNT不加DISTINCT
FROM (
  -- 内层:按(dt, channel_id, user_id)去重,粒度更细,天然打散
  SELECT DISTINCT dt, channel_id, user_id
  FROM user_events
) t
GROUP BY dt, channel_id;
 
-- 【正确改法二】:approx_count_distinct(近似,误差约1-5%)
SELECT
  dt,
  channel_id,
  approx_count_distinct(user_id) AS dau_approx
FROM user_events
GROUP BY dt, channel_id;
-- 适用场景:趋势监控、大盘分析(允许1-5%误差)
-- 不适用:精确结算、财务报表

面试时主动说的加分项:子查询方案精确但有两次Shuffle的开销;approx_count_distinct用HyperLogLog算法,只需一次Pass,速度更快,但有误差。选哪个取决于业务对精确度的要求。」说出这句,立刻显示你理解两种方案的trade-off。

手写题二:GROUP BY热点key两阶段聚合

场景:统计每个广告渠道的总收入,某个渠道数据量是其他的200倍,GROUP BY严重倾斜,请优化。

-- 题目:优化下面有倾斜的GROUP BY
-- SELECT channel_id, SUM(revenue) AS total_revenue
-- FROM ad_events WHERE dt='2024-08-01' GROUP BY channel_id;
 
-- 【两阶段聚合标准模板】
SELECT
  channel_id,
  SUM(partial_revenue) AS total_revenue  -- 第二阶段:汇总各分片的局部结果
FROM (
  SELECT
    channel_id,
    -- 加随机盐:把channel_id打散成N份(这里N=10)
    CONCAT(CAST(FLOOR(RAND() * 10) AS STRING), '_', channel_id) AS salted_key,
    SUM(revenue) AS partial_revenue  -- 第一阶段:局部SUM
  FROM ad_events
  WHERE dt = '2024-08-01'
  GROUP BY
    channel_id,
    CONCAT(CAST(FLOOR(RAND() * 10) AS STRING), '_', channel_id)
) t
GROUP BY channel_id;  -- 第二阶段:按真实channel_id汇总
 
-- 关键点说明:
-- 1. RAND()*10 → 前缀0-9,每个channel_id的数据被打散到10个Task
-- 2. 第一阶段GROUP BY包含salted_key(打散)和channel_id(保留真实key)
-- 3. 第二阶段只按channel_id GROUP BY(还原并汇总10份结果)
-- 4. 只适用于SUM/COUNT/MAX/MIN等满足结合律的聚合函数

面试时主动说的加分项:「N(随机前缀数量)越大,打散效果越好,但第二阶段的数据量也越大。如果倾斜严重(某个key占50%),N可以设50甚至100;一般场景N=10够了。另外这个方案产生了N倍的中间数据,如果基表本来数据量就大,要注意中间存储开销。」

手写题三:JOIN倾斜处理(综合版)

场景:orders大表(10亿行)JOIN users小表(500万行),按user_id关联,某个user_id有100万条订单导致倾斜,同时orders的user_id有10%是NULL。请优化。

-- 完整的JOIN倾斜处理:NULL过滤 + 热点key单独处理 + 其余Broadcast/正常JOIN
 
-- 先确认users表大小:500万行 × 每行100字节 ≈ 500MB,偏大,先不Broadcast
-- 热点key:已知user_id='hot_user_001'有100万条订单
 
-- 方案:三路UNION ALL
-- 路径A:hot_user,单独处理(Broadcast users对应行)
SELECT /*+ BROADCAST(u_hot) */
  o.order_id, o.user_id, o.amount, u_hot.user_name
FROM (
  SELECT * FROM orders
  WHERE user_id = 'hot_user_001'  -- 只取热点key
) o
JOIN (
  SELECT * FROM users WHERE user_id = 'hot_user_001'  -- 只取对应1行
) u_hot ON o.user_id = u_hot.user_id
 
UNION ALL
 
-- 路径B:非热点、非NULL的正常用户,走普通JOIN
SELECT o.order_id, o.user_id, o.amount, u.user_name
FROM (
  SELECT * FROM orders
  WHERE user_id != 'hot_user_001'  -- 排除热点key
    AND user_id IS NOT NULL          -- 排除NULL
) o
JOIN users u ON o.user_id = u.user_id
 
UNION ALL
 
-- 路径C:user_id为NULL的订单,直接返回(不参与JOIN)
SELECT order_id, user_id, amount, NULL AS user_name
FROM orders
WHERE user_id IS NULL;
 
-- 三路UNION ALL覆盖了所有数据,结果完整且正确

面试时主动说的加分项:「三路UNION ALL的方案代码最清晰,但有两个JOIN和一个全表扫描,性能上orders表被扫了三次(或者用CTE缓存后扫一次)。如果这个SQL会高频运行,可以把orders预先物化为临时表,避免重复扫描。」

现场手写的答题策略

面试手写SQL的答题节奏很重要,按这个步骤来,即使中间有卡顿也不会失控:

步骤时间做什么说什么
读题+分析30秒识别倾斜类型(GROUP BY/JOIN/NULL)「这个是COUNT(DISTINCT)倾斜,不能直接加盐,需要改成子查询」
说思路30秒口述解决方案,不急着写「我的方案是先按(dt,channel,user_id)去重,外层再COUNT」
写SQL2-3分钟按模板写,先写结构再填细节边写边说:「内层去重,外层COUNT」
检查+补充30秒检查语法,主动说出局限「这个方案有两次Shuffle,如果业务允许误差可以用approx代替」

手写时最忌讳的是:拿到题就埋头写,写到一半发现思路不对,又删了重写,浪费时间还显得没把握。先口述思路,让面试官确认方向对了再动笔,效率最高。

面试高频追问

追问一:你写的两阶段聚合,如果RAND()每次调用返回不同的值,GROUP BY结果会对吗?

这是一个非常刁钻的追问!RAND()在SQL里的行为取决于引擎:在Spark SQL里,RAND()在同一行的多次引用返回相同的值(同一个执行计划节点内),所以SELECT里的RAND()和GROUP BY里的RAND()是同一次调用的结果,不会不一致。但如果你先SELECT CONCAT(RAND(),'_',id) AS key,再在外层GROUP BY key,内外是不同的查询,RAND()会不同——这就是为什么两阶段聚合要把内层的CONCAT写成子查询,让外层引用子查询的结果(确定性的值),而不是在外层再调用一次RAND()。如果面试官追问这个,能答上来说明你真的理解了SQL的执行模型。

追问二:你刚才写的三路UNION ALL,如果hot_user_id不止一个而是动态变化的,怎么办?

动态热点key的场景,三路UNION ALL的方案就不适用了(因为需要硬编码热点key)。替代方案:1)提前通过排查SQL找到当天的TOP N热点key,写入一张临时表hot_keys;2)正常路路和NULL路不变;热点路改为JOIN hot_keys表来动态识别:SELECT * FROM orders WHERE user_id IN (SELECT user_id FROM hot_keys)。3)或者直接用AQE兜底,降低skewedPartitionFactor阈值让AQE更积极地处理动态热点,不需要手动识别热点key。如果追问,能说出「静态热点用UNION ALL,动态热点用AQE或者动态hot_keys表」,就展示了方案的完整性。

追问三:现场手写时遇到语法卡壳怎么办?

诚实地说出来,不要卡在那里沉默。可以说:「EXPLODE的具体语法我记不太清,大意是lateral view explode把数组展开,我写一个伪代码,实际用时查一下文档」。面试官通常可以接受这种方式,因为他们考察的是思维方式,不是记忆力。更重要的是,如果某个细节不确定,主动说出来然后绕过去,比因为一个语法问题卡死整个答题好得多。我面试时也遇到过记不清Hive的lateral view语法的情况,就用「展开数组的操作」描述了一下,面试官说「我知道你说的什么」,然后继续追问了更深的问题。

实战:我准备面试时打磨手写题的方法

我在准备冲大厂的面试时,专门整理了一套手写SQL的训练方法:

# 训练方法(每天30分钟,坚持2周):
 
# 第一周:建立模板肌肉记忆
# 每天手写(不是复制粘贴)以下三个模板各一遍:
# 1. COUNT(DISTINCT)拆子查询
# 2. 两阶段聚合加盐打散
# 3. 三路UNION ALL(热点key + 正常key + NULL)
# 写完后对比参考答案,找出差异,记录容易写错的地方
 
# 第二周:变题练习
# 每天找一道新的场景题,用第一周练出来的模板改写
# 变体1:COUNT(DISTINCT)有两个(同时统计UV和设备数)
# 变体2:GROUP BY有多个key(channel+country),都有倾斜
# 变体3:LEFT JOIN有倾斜,且需要保留左表的所有行
 
# 检验标准:
# 能在3分钟内手写出正确的两阶段聚合SQL
# 能在4分钟内手写出完整的三路UNION ALL
# 写完后能主动说出3个潜在问题或局限性
-- 变体练习示例:同时有COUNT(DISTINCT)和SUM,channel_id倾斜
-- 原始(倾斜):
-- SELECT channel_id, COUNT(DISTINCT user_id) AS uv, SUM(revenue) AS gmv
-- FROM orders GROUP BY channel_id;
 
-- 优化:UV和GMV分开处理,最后JOIN
SELECT uv.channel_id, uv.uv, gmv.gmv
FROM (
  -- UV:子查询拆COUNT(DISTINCT)
  SELECT channel_id, COUNT(user_id) AS uv
  FROM (SELECT DISTINCT channel_id, user_id FROM orders) t
  GROUP BY channel_id
) uv
JOIN (
  -- GMV:两阶段聚合处理SUM倾斜
  SELECT channel_id, SUM(partial_gmv) AS gmv
  FROM (
    SELECT channel_id,
      CONCAT(CAST(FLOOR(RAND()*10) AS STRING),'_',channel_id) AS sk,
      SUM(revenue) AS partial_gmv
    FROM orders
    GROUP BY channel_id, CONCAT(CAST(FLOOR(RAND()*10) AS STRING),'_',channel_id)
  ) t
  GROUP BY channel_id
) gmv ON uv.channel_id = gmv.channel_id;
-- 这个变体同时用了两种技巧,是高频的「综合题」

总结

面试手写SQL,模板在胸就不慌。三类最高频手写题:COUNT(DISTINCT)拆子查询、GROUP BY两阶段加盐、JOIN三路UNION ALL。手写顺序:读题30秒 → 口述思路30秒 → 写SQL 2-3分钟 → 主动说局限30秒。训练方法:第一周建立模板肌肉记忆,第二周变题练习。写完代码主动说trade-off,是拿高分的关键——面试官要的不只是「能写出来」,而是「知道自己在做什么」。

下一篇聊倾斜问题的「数据量级」——不同数据规模下,倾斜的处理策略有什么不同?百GB和百TB的场景,方案选择有哪些差异?

你们面试时遇到过哪些刁钻的倾斜手写题?评论区分享题目,我来给出参考答案~

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

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