订阅更新

你们数仓分几层?每层放什么?——不要再背 ODS/DWD/DWS/ADS 四个词了

数仓开发手册 第 2 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. 业务场景:我在某元宇宙社交应用的分层实践
  3. 段落一:ODS——贴源层,最重要的原则是『不要清洗』
  4. 段落二:DWD——数仓的心脏,出错最贵
  5. 段落三:DWS——按粒度聚合,抗住下游的多样化需求
  6. 段落四:ADS——应用层,面试重灾区
  7. 面试高频追问
  8. 实战踩坑:ADS 层 SQL 1200 行没人敢改
  9. 总结

先说结论

分层的本质是『解耦和复用』,不是为了分层而分层。解耦是让上游变更不影响下游,复用是让同一份加工逻辑被多个应用方用。没有这两个目的,分层就是在增加成本。

四层是常见做法,但不是标准答案。很多公司还有 DIM 层(维度层)、TMP 层(临时层)、MID 层(中间层)。面试时讲你们实际的分层,不要照搬网上的标准。

DWD 是命根子,ADS 是雷区。DWD 做错,全链路都是错的;ADS 堆逻辑,最后没人敢改。这两层是面试官最爱追问的。

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

详细讲解

几乎每一场数仓面试都会问分层。大多数候选人的回答是:『分四层,ODS 贴源层、DWD 明细层、DWS 汇总层、ADS 应用层』——然后没了。面试官心里已经给你打了『背过定义』四个字。真正想考的是:你为什么要分层、哪些场景可以不分层、每层的边界在哪里、哪一层最容易出事。

业务场景:我在某元宇宙社交应用的分层实践

我在某元宇宙社交应用做实时数仓时,用的是 ODS / DWD / DWS / ADS 四层 + DIM 维度层 + TMP 临时层。当时业务是虚拟礼物交易、用户行为分析,核心诉求是实时大屏和次日看板。

-- ODS 贴源:原样接入,不做任何清洗
-- 只做格式转换(JSON → 结构化)和分区
CREATE TABLE ods_gift_send_log (
  uid BIGINT, gift_id BIGINT, amount DECIMAL(10,2),
  ts TIMESTAMP, raw_json STRING  -- 保留原始报文
) PARTITIONED BY (dt STRING, hh STRING);
 
-- DWD 明细:清洗 + 规范 + 去重 + 关联基础维度
CREATE TABLE dwd_gift_send_detail (
  uid BIGINT,
  country_code STRING,  -- 从 DIM 补充的维度
  gift_name STRING,     -- 从 DIM 补充的维度
  amount_usd DECIMAL,   -- 统一币种
  ts TIMESTAMP, dt STRING
) PARTITIONED BY (dt STRING);
 
-- DWS 汇总:按粒度聚合(uid 日、房间日、国家日)
CREATE TABLE dws_user_gift_daily (
  uid BIGINT, send_cnt INT, send_amount_usd DECIMAL
) PARTITIONED BY (dt STRING);
 
-- ADS 应用:直接给 BI / 接口用,做最后一公里拼接
CREATE TABLE ads_dashboard_daily (...);

这里面最值得说的是 DIM 层。很多小公司把国家表、礼物表这种维度表塞到 DWD 里,看起来省一层,实际上每次维度变更都要重跑 DWD,代价极高。把维度单独一层之后,维度变更只影响 join 逻辑不影响历史数据。

段落一:ODS——贴源层,最重要的原则是『不要清洗』

ODS 是整个数仓的入口,唯一的任务是『把原始数据原样接进来』。这里最常见的错误是在 ODS 做清洗——NULL 补零、字段去重、格式化——看似省事,实际是埋雷。

为什么不能在 ODS 清洗:ODS 要保留 100% 的原始信息,因为你不知道以后需要从原始数据里捞出什么。一旦在 ODS 做了有损清洗,原始信息丢了就再也找不回来。

-- ❌ 错误做法:在 ODS 就做清洗
INSERT INTO ods_order
SELECT uid,
  COALESCE(amount, 0) AS amount,  -- 补零了原始 NULL 丢失
  UPPER(country) AS country        -- 格式化了原值丢失
FROM kafka_source;
 
-- ✅ 正确做法:ODS 只做结构化
INSERT INTO ods_order
SELECT uid, amount, country, raw_json  -- 保留原始报文
FROM kafka_source;
 
-- 清洗放到 DWD
INSERT INTO dwd_order
SELECT uid,
  COALESCE(amount, 0) AS amount,
  UPPER(TRIM(country)) AS country
FROM ods_order WHERE dt = '${bizdate}';

段落二:DWD——数仓的心脏,出错最贵

DWD 明细层是数仓的『事实真相』。这一层做错了,上面所有 DWS 和 ADS 都要重跑。我在某出海硬件品牌做埋点治理时,把 1000+ 事件精简到 100+ 标准化事件,做的就是 DWD 层的标准化——这件事做好了,数据质量问题减少 60%。

DWD 的三个核心动作:清洗(NULL / 去重 / 类型修复)、规范(字段命名 / 口径统一 / 币种时区统一)、关联维度(补充可枚举的维度字段,减少下游重复 join)。

DWD 做什么DWD 不做什么理由
NULL / 脏数据清洗主观业务规则过滤过滤逻辑放 DWS 或 ADS,DWD 要全量
字段命名规范化跨事实表聚合聚合是 DWS 的事
补充稳定维度补充频繁变化的维度频繁变化的留到 DWS 现 join
去重(exactly once)按业务时间切分时间切分应在 ODS 分区阶段做

段落三:DWS——按粒度聚合,抗住下游的多样化需求

DWS 汇总层的关键词是『粒度』。同一个事实表往往要做多个粒度的汇总:用户日粒度、用户周粒度、国家日粒度、设备日粒度。一个通用的经验:常用的 4-5 个粒度预聚合,不常用的留到 ADS 现算。

-- 同一个 DWD 生成多个粒度的 DWS
 
-- 用户日粒度(最细)
CREATE TABLE dws_user_gift_daily AS
SELECT dt, uid, COUNT(*) cnt, SUM(amount_usd) amt
FROM dwd_gift_send_detail GROUP BY dt, uid;
 
-- 国家日粒度(聚合)
CREATE TABLE dws_country_gift_daily AS
SELECT dt, country_code, COUNT(DISTINCT uid) uv, SUM(amount_usd) amt
FROM dwd_gift_send_detail GROUP BY dt, country_code;
 
-- 用户周粒度(累计)
CREATE TABLE dws_user_gift_weekly AS
SELECT week_of_year, uid, SUM(amount_usd) amt
FROM dwd_gift_send_detail GROUP BY week_of_year, uid;

段落四:ADS——应用层,面试重灾区

ADS 层是面试官最爱挖的雷。最常见的问题是 ADS 里堆了几百行复杂 SQL,后来的人不敢改——这是很多公司的真实情况。

ADS 的黄金法则:ADS 只做『最后一公里拼接』,不做复杂计算。如果你发现 ADS 的 SQL 超过 200 行,大概率是 DWS 层没建好,需要回到 DWS 补一层汇总。

另一个雷区:ADS 表口径不一致。同一个『GMV』指标,看板 A 用 ads_gmv_dashboard,看板 B 用 ads_gmv_report,两个表算出来差 3%——这就是同指标多部门三个数的经典场景(第 15 篇会细讲)。

面试高频追问

追问一:为什么不能所有业务都直接从 ODS 写到 ADS?

可以,但只能适用于极简场景,比如日志存档查询。稍微复杂一点就会暴露三个问题:(1)ODS 格式一变,所有下游全挂;(2)同样的清洗逻辑在每个 ADS 里复制一遍,口径难保证一致;(3)ADS SQL 膨胀到几千行,无法维护。分层的本质是解耦和复用,不分层就是在用未来的维护成本换眼前的开发速度。

追问二:DIM 层和 DWD 层有什么区别?维度为什么要单独一层?

DWD 是事实表(发生过的动作:下单、点击),DIM 是维度表(描述事物:用户、商品、国家)。分开的核心原因是『变更频率不同』:事实表是追加写入,维度表是更新覆盖(SCD)。放在一起会导致每次维度变更都要重跑事实表。另外 DIM 层在实时场景下通常广播 join 或用 lookup join,和 DWD 的处理方式也完全不一样。

追问三:你们层与层之间是 T+1 调度还是实时的?

我在某元宇宙社交应用是双链路:离线走 ODS→DWD→DWS→ADS 的 T+1,实时走 Kafka→Flink→Doris 的分钟级。两条链路口径要对齐,用离线做 T+1 的最终口径、实时做当日看板,隔天用离线修正。这个设计在第 18 篇实时数仓那一篇会细讲。

实战踩坑:ADS 层 SQL 1200 行没人敢改

我在某短剧出海公司接手过一个 ADS 表,SQL 1200 行,原作者离职了,所有的核心业务看板都依赖它。我花了一周才看懂它在算什么——本质上是六个 DWS 都没建全,所有聚合逻辑全塞在 ADS 一条 SQL 里。

解决办法是往 DWS 层补了三张中间汇总表,ADS 从 1200 行拆成了 80 行。之前改一次要测一整天,现在改完 30 分钟就能上。这个故事在第 14 篇 SQL 1200 行 code review 会完整讲一遍。

Plain

重构前:ADS 层 1200 行 SQL
  - 4 个 CTE 做用户维度聚合
  - 3 个 CTE 做订单事实聚合
  - 5 个 JOIN 做口径拼接
  - 最终 SELECT 做维度交叉
 
重构后:DWS 补 3 张表 + ADS 80 行
  - dws_user_order_daily(用户日粒度)
  - dws_country_order_daily(国家日粒度)
  - dws_sku_order_daily(商品日粒度)
  - ADS 只做最后的口径拼接

总结

回答分层问题,不要只报 ODS/DWD/DWS/ADS 四个词,要讲清楚三件事:(1)为什么分层,核心是解耦和复用;(2)你们实际怎么分,包括 DIM/TMP 这种扩展层;(3)你踩过的坑,比如 ODS 做清洗、ADS 堆逻辑、DIM 没独立分层。能把这三件事讲清楚,这道题就拿到 9 分。

下一篇:『你们调度是怎么做的?』——这是第三道必问题。很多人回答『用 DataWorks』就停了,其实面试官想听的是依赖关系、重跑机制、告警策略。

你们公司数仓分了几层?有没有那种明明应该独立、却被硬塞到其他层的『杂货层』?

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

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