订阅更新

一个 1200 行的 SQL 怎么 code review?——从代码层面看架构能力

数仓开发手册 第 14 题 约 10 分钟读完

本文目录
  1. 先说结论
  2. 业务场景:我在某短剧出海公司接手的那个 1200 行 SQL
  3. 段落一:第一步——看输出搞清楚要什么
  4. 段落二:第二步——看骨架(CTE 粒度图)
  5. 段落三:第三步——看细节(口径一致性)
  6. 段落四:重构——把 1200 行压到 80 行
  7. 段落五:Code Review 清单(我们团队的硬规则)
  8. 面试高频追问
  9. 实战踩坑:重构完下游有人偷偷 JOIN 了中间临时表
  10. 总结

先说结论

Review 千行 SQL 的三步法:(1)先看输出(最终 SELECT 在算什么);(2)再看骨架(有几个 CTE、每个 CTE 算什么粒度);(3)最后看细节(JOIN 条件 / 去重 / 过滤)。从果追因,不是从头读。

判断『该不该拆』的三个信号:超过 5 个 CTE、出现 3 次以上的相似子查询、最外层 SELECT 超过 50 个字段。出现任意一个就该考虑拆成中间表。

最重要的是识别口径漂移。1200 行里经常有 3-4 处同一个指标用了不同的算法——这是最容易出数据问题的地方。

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

详细讲解

面试到这个阶段,面试官开始考察『你会不会看别人的代码』。题目通常是:『给你一个前同事留下的 1200 行 SQL,业务逻辑不清楚,你怎么 review?』这题看起来是代码题,实际考架构能力——你能不能在一团乱麻里看到结构、抓住口径、发现设计缺陷。答好了基本能进 senior 档。

业务场景:我在某短剧出海公司接手的那个 1200 行 SQL

前面好几篇都埋过这个钩子——我在某短剧出海公司接手过一个 ADS 表,SQL 1200 行,原作者离职,所有核心看板都依赖它。我花了一周才看懂它在算什么,重构之后从 1200 行压到 80 行,ETL 时间从 24 小时跑不完变成 8 分钟。现在完整讲一下这个 review 和重构的过程。

-- 一个典型的 1200 行 ADS SQL 的骨架
WITH 
  -- 第 1-200 行:6 个用户维度 CTE
  user_base AS (...),
  user_region AS (...),
  user_device AS (...),
  user_vip AS (...),
  user_first_behavior AS (...),
  user_churn_status AS (...),
 
  -- 第 201-500 行:4 个订单事实 CTE
  order_daily AS (...),
  order_weekly AS (...),
  order_by_category AS (...),
  order_refund AS (...),
 
  -- 第 501-900 行:5 个汇总 CTE(各种交叉)
  metric_daily_by_country AS (
    SELECT ... FROM user_base 
    JOIN order_daily ... 
    JOIN user_region ...
    -- 80 行
  ),
  metric_weekly ... ,
 
  -- 第 901-1200 行:最终拼装
  SELECT * FROM metric_daily_by_country
  LEFT JOIN metric_weekly ...
  LEFT JOIN ...
  -- 50 个输出字段

段落一:第一步——看输出搞清楚要什么

永远不要从第一行读起。1200 行你读到 300 行就放弃了,而且中间的 CTE 脱离终点看不懂意图。先跳到最后的 SELECT,看它输出什么字段、给谁用。

-- 先找最外层 SELECT
SELECT 
  country, dt,
  dau, wau_7d, mau_30d,      -- 活跃指标
  gmv_daily, gmv_7d, gmv_30d, -- GMV 指标
  order_cnt, refund_rate,
  -- ... 50 个字段
FROM ...;
 
-- 从输出倒推意图:
-- 这是一张 '日粒度 × country 维度' 的运营看板表
-- 活跃指标 + 变现指标 + 订单指标三块
-- 看板维度:按国家 / 按日期看

做这一步的收益:搞清楚输出之后,下面读 CTE 时每个 CTE 都有『定位』——知道它是为了支撑最终哪些字段。

段落二:第二步——看骨架(CTE 粒度图)

把每个 CTE 的粒度写下来。粒度决定了它能被谁引用,也暴露了很多问题。

Plain

CTE 粒度图(review 时画出来):
 
  user_base           粒度: uid
  user_region         粒度: uid       (user_base 的补充)
  user_device         粒度: uid       (user_base 的补充)
  user_vip            粒度: uid       (user_base 的补充)
  user_first_behavior 粒度: uid
  user_churn_status   粒度: uid
  → 6 个 CTE 都是 uid 粒度,合并成 1 个用户维度聚合!
 
  order_daily         粒度: uid × dt
  order_weekly        粒度: uid × week
  order_by_category   粒度: uid × category × dt
  order_refund        粒度: uid × dt
  → 4 个 CTE 是不同粒度的订单聚合
 
  metric_daily_by_country  粒度: country × dt
  metric_weekly            粒度: country × week
  ...

看到这个粒度图立刻就能发现问题:6 个 uid 粒度的 CTE 重复 scan 用户表 6 次!这是最大的性能黑洞。应该合并成一个 dws_user_wide,scan 一次。

段落三:第三步——看细节(口径一致性)

骨架清楚后就开始看关键细节,重点是口径。1200 行的 SQL 里同一个指标被不同人加过代码,口径可能漂移。

-- 问题 1: DAU 在两处定义不一样
-- user_base 里:DAU = count(distinct uid) where dt
-- order_daily 里:DAU = count(distinct uid) where amount > 0
-- 一个是全量活跃,一个是付费活跃,但最外层都叫 dau
 
-- 问题 2: 去重粒度不一致
-- 有些地方是 count(distinct uid)
-- 有些地方是 count(distinct concat(uid, dt))
-- 如果数据已经是 uid × dt 唯一的,后者多此一举但结果相同
-- 如果不唯一,两者结果不同
 
-- 问题 3: JOIN 类型混用
-- INNER JOIN user_region(注册国家)
-- LEFT JOIN user_device(设备)
-- → user 可能因为没有 region 被过滤掉(用户数少了 5%)

段落四:重构——把 1200 行压到 80 行

Review 完发现问题就该动手了。思路:把 CTE 按粒度拆成独立的 DWS 中间表,ADS 只做最后拼接。

-- 重构方案:补 3 张 DWS 表
 
-- DWS 1: 用户维度聚合(替代 6 个 uid 粒度 CTE)
CREATE TABLE dws_user_profile AS
SELECT uid, register_country, device_model, vip_level,
  first_play_dt, churn_status
FROM dim_user u
LEFT JOIN user_device_profile ON ...
LEFT JOIN user_vip_current ON ...;
 
-- DWS 2: 订单日粒度(替代 4 个订单 CTE 的一部分)
CREATE TABLE dws_user_order_daily AS
SELECT uid, dt, SUM(amount) gmv, COUNT(*) order_cnt,
  SUM(refund_amount) refund_amt
FROM fact_order GROUP BY uid, dt;
 
-- DWS 3: 国家日粒度汇总(替代大部分 metric_ CTE)
CREATE TABLE dws_country_metric_daily AS
SELECT p.register_country country, o.dt,
  COUNT(DISTINCT o.uid) dau, SUM(o.gmv) gmv_daily, ...
FROM dws_user_profile p JOIN dws_user_order_daily o USING (uid)
GROUP BY p.register_country, o.dt;
 
-- ADS 从 1200 行 → 80 行
CREATE TABLE ads_dashboard AS
SELECT country, dt, dau, gmv_daily,
  wau_7d, mau_30d, ...
FROM dws_country_metric_daily
LEFT JOIN dws_country_metric_weekly USING (country, dt)
LEFT JOIN dws_country_metric_monthly USING (country, dt);

段落五:Code Review 清单(我们团队的硬规则)

Plain

我制定的 SQL Code Review Checklist:
 
【结构】
  □ 单个 SQL 不超过 300 行(超过要拆 DWS)
  □ CTE 不超过 5 个(超过要拆中间表)
  □ 嵌套子查询不超过 3 层
 
【正确性】
  □ 所有 JOIN 都指定了 JOIN 类型(INNER/LEFT/...)
  □ JOIN 条件包含分区字段(a.dt = b.dt)
  □ 去重粒度明确(单字段 or 联合)
  □ 核心指标在注释里说明口径
 
【性能】
  □ 所有分区表都带分区过滤
  □ 大表 JOIN 小表考虑 Broadcast Hint
  □ 没有 SELECT *(除非 CTAS)
  □ 幂等(INSERT OVERWRITE 或 MERGE)
 
【维护性】
  □ 关键逻辑有中文注释
  □ 魔法数字(0.3、86400)有注释说明

面试高频追问

追问一:你花了一周看懂 1200 行,业务催得急怎么办?

真实经验:(1)前两天先不改代码,只做 review 和画骨架图;(2)业务催得急时先不要大重构,先修具体的业务方反映的问题(比如某个指标算错了)——只改涉及的 CTE,其他地方不动;(3)重构做成独立项目,和日常需求分开排期,一般拉 1-2 个迭代周期专门做。千万不要在重构中间接业务需求——会把两件事都搞砸。

追问二:你怎么验证重构前后数据一致?

双跑对比。让老 SQL 和新 SQL 同时跑 7 天,每天对比每个指标的差异。差异 > 0.1% 的指标逐一排查。我在那次重构里双跑了 2 周,调整了 3 次口径才对齐。千万不要『重构完了直接切』——100% 会出问题。

追问三:有没有遇到过『重构后更慢』的情况?

遇到过。重构之后把一条 SQL 拆成 3 张 DWS + ADS,看起来优雅,但加起来总跑时间比原来还长。原因:原来的 1200 行 SQL 虽然难懂,优化器一次性看到所有逻辑能做全局优化;拆开之后每张 DWS 单独优化,中间结果落盘又读出来,IO 变多了。解决:拆的时候要评估拆分后的总成本,不是所有情况都适合拆。一般只有当 CTE 被多次复用(比如 3+ 张 ADS 都要用)时拆 DWS 才划算。

实战踩坑:重构完下游有人偷偷 JOIN 了中间临时表

我那次 1200 行重构做完后,下线老表时发现——有个 BI 同学发现老 SQL 里的一个 CTE 输出(本来是临时的)被他 INSERT 到了自己的表,做了个私有看板。我下线老表的 ETL 把他的看板打挂了。

教训:下线老资产前必须做依赖扫描。手段有几个:(1)Atlas / DataWorks 血缘;(2)SQL 日志扫描 `grep 老表名` 找出所有引用;(3)公告一周缓冲期让下游迁移。从那以后我们团队的下线 SOP 里加了两周公告期的硬要求。

总结

1200 行 SQL code review 这道题答好的关键是『讲出方法论而不只是技巧』。三步法(输出 → 骨架 → 细节)+ CTE 粒度图 + 口径漂移识别 + code review checklist + 一个真实重构故事(1200 行压到 80 行、24 小时变 8 分钟),这题就是 9-10 分水平。

下一篇:『同一个指标,三个部门三个数——怎么处理?』——生产问题和架构话题的交界点,也是『口径治理』这个永恒话题的起点。

你接手过最长的 SQL 多少行?后来是重构了还是躺着用?

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

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