订阅更新

面试被问数仓分层?这样答才能拿高分

数仓建模 20 讲 · 第 1 讲 约 11 分钟读完

本文目录
  1. 先搞清楚:为什么要分层?
  2. 四层架构,每层到底干什么?
  3. 面试怎么答才能拿高分?
  4. 追问防线:面试官常见追问
  5. 总结

数仓分层这道题,面试出现率大概90%以上。 image.png

但真正答得好的人不到10%。大多数人的回答都是这样的:「ODS是原始层,DWD是明细层,DWS是汇总层,ADS是应用层」——四句话说完,面试官面无表情,你心里也知道凉了。 image.png

为什么?因为面试官要的不是背书,而是你对分层架构的理解深度和实战经验。

这篇文章,我把数仓分层从"是什么"到"怎么用"到"怎么在面试里讲出彩"一次讲透。


先搞清楚:为什么要分层?

很多人上来就开始背ODS/DWD/DWS/ADS,却说不清分层的目的是什么。

数仓分层的核心思想就四个字:以空间换时间。 image.png

想象一下,如果没有分层,业务方每提一个需求,你就从原始数据开始写一条SQL:清洗、关联、聚合、输出,全部一步到位。第一次还好,第二个需求来了呢?大概率要重复80%的清洗和关联逻辑。再来第三个、第四个……你会发现自己在不停地重复造轮子,而且一旦某个源表的格式变了,你要改十几条SQL。

分层就是把数据处理拆成多个阶段,每一层只干一件事。下游直接复用上游的成果,不用每次从头来。

我喜欢用做饭来类比,一下就能记住: image.png

  • ODS:菜市场买回来的生鲜——原封不动,什么样就什么样
  • DWD:洗净切好的食材——清洗、标准化、该去皮的去皮
  • DWS:做好的半成品菜——红烧肉已经炖好了,热一下就能吃
  • ADS:端上桌的成品——摆好盘直接上桌,客人拿起筷子就能吃

你不会每次做饭都从杀鸡拔毛开始吧?数仓也一样。


四层架构,每层到底干什么?

ODS层:原始数据快照

ODS的全称是Operational Data Store,操作数据存储层。 image.png

这层的原则就一个字:不动。

从MySQL、埋点日志、第三方API同步过来的数据,原封不动地存进来。字段名不改,数据类型不转,NULL值不处理,脏数据也先留着。它的作用是给数仓留一份"原始底片",任何时候出了问题都能回溯到最原始的数据。

实际工作中的样子:

-- 命名规范:ods_{来源系统}_{表名}_{加载方式}
-- di = daily increment 每日增量
-- df = daily full 每日全量

ods_mysql_order_di          -- MySQL订单表,每日增量同步
ods_mysql_user_df           -- MySQL用户表,每日全量同步
ods_log_click_di            -- 埋点点击日志,每日增量
ods_api_facebook_ads_di     -- Facebook广告数据API,每日拉取

ODS层的几个关键细节:

第一,增量还是全量?数据量小的维度表(比如城市表、渠道表)通常全量同步,每天覆盖就行。数据量大的流水表(比如订单、日志)必须增量同步,不然每天全量拉几十亿行不现实。

第二,保留多久?看公司要求。我在出海硬件品牌的时候ODS保留90天,短剧出海这边保留30天。时间再长的话存储成本扛不住,而且真正需要回溯半年前原始数据的场景极少。

第三,分区怎么设?通常按天分区(dt字段),这是最通用的做法。个别数据量特别大的表(比如每天几亿行的日志)可以加小时分区。

DWD层:明细清洗层(最重要的一层)

DWD的全称是Data Warehouse Detail,明细数据层。 image.png

这层是数仓的核心苦力层,干的活最多也最重要。如果ODS是生鲜,DWD就是洗菜切菜——看起来不起眼,但做不好后面全白搭。

DWD层要做的事情:

① 数据清洗

-- NULL值处理
COALESCE(user_name, '未知用户') AS user_name

-- 异常值过滤
WHERE order_amount > 0                   -- 过滤金额为负的异常订单
  AND order_amount < 1000000             -- 过滤明显不合理的大金额

-- 数据去重(同一笔订单可能因为重试被采集两次)
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY update_time DESC) = 1

② 字段标准化

不同来源系统对同一个概念的命名和格式可能完全不一样,DWD层要统一成一套标准。

-- 性别字段:来源A写的是 M/F,来源B写的是 1/0,来源C写的是 male/female
CASE 
    WHEN gender IN ('M', '1', 'male')   THEN '男'
    WHEN gender IN ('F', '0', 'female') THEN '女'
    ELSE '未知'
END AS gender_std

-- 时间格式统一
FROM_UNIXTIME(create_time / 1000, 'yyyy-MM-dd HH:mm:ss') AS create_time_std

③ 维度关联

在DWD层把常用的维度信息关联上来,下游不用每次都去JOIN维度表。

SELECT
    o.order_id,
    o.user_id,
    u.user_name,
    u.city,
    u.register_channel,
    o.product_id,
    p.product_name,
    p.category,
    o.order_amount,
    o.create_time
FROM ods_order_di o
LEFT JOIN dim_user u ON o.user_id = u.user_id
LEFT JOIN dim_product p ON o.product_id = p.product_id
WHERE o.dt = '${bizdate}'

④ 跨时区处理(出海业务的痛点)

这一点我要多说两句,因为这是我在短剧出海公司DWD层花时间最多的部分。

海外用户遍布全球几十个时区,埋点数据和服务端日志的时间戳通常是UTC。如果DWD层不做时区转换,后面的留存率、日活、次日留存这些指标全是错的——美国用户晚上11点的行为被算到了第二天,留存率直接虚高。

-- UTC转用户当地时间
-- 根据用户注册时的IP归属国家确定时区
FROM_UTC_TIMESTAMP(event_time, user_timezone) AS local_event_time

-- 用转换后的本地时间来确定"哪一天"
DATE(FROM_UTC_TIMESTAMP(event_time, user_timezone)) AS local_date

这件事看起来简单,但坑特别多:夏令时切换、用户跨国旅行、部分国家有半小时时区偏移(比如印度是UTC+5:30)。我们后来的做法是在DWD层统一转成两个字段:event_time_utc(保留原始UTC)和event_time_local(按用户归属国时区转换),下游按需取用。

DWD层命名规范:

dwd_{业务域}_{数据域}_{加载方式}

dwd_trade_order_detail_di       -- 交易域-订单明细-每日增量
dwd_user_login_detail_di        -- 用户域-登录明细-每日增量
dwd_ad_click_detail_di          -- 广告域-点击明细-每日增量

DWS层:主题汇总层

image.png

DWS的全称是Data Warehouse Summary,汇总数据层。

DWD层的数据粒度还是很细的(一行一条记录),到了DWS层就要按照业务主题做预聚合。

比如下游经常需要看"每个用户每天的行为汇总",与其让10个报表各自写一遍聚合SQL,不如在DWS层提前算好。

-- 典型DWS表:用户行为日汇总
CREATE TABLE dws_user_behavior_1d AS
SELECT
    user_id,
    dt,
    COUNT(DISTINCT session_id)                       AS session_cnt,      -- 会话数
    SUM(CASE WHEN event = 'click' THEN 1 ELSE 0 END) AS click_cnt,       -- 点击次数
    SUM(CASE WHEN event = 'play' THEN 1 ELSE 0 END)  AS play_cnt,        -- 播放次数
    SUM(play_duration)                                AS total_play_sec,  -- 总播放时长(秒)
    SUM(order_amount)                                 AS total_pay_amount,-- 总支付金额
    MIN(event_time)                                   AS first_active_time,-- 当日首次活跃
    MAX(event_time)                                   AS last_active_time  -- 当日末次活跃
FROM dwd_user_behavior_detail_di
WHERE dt = '${bizdate}'
GROUP BY user_id, dt;

DWS层的设计原则:

第一,按主题组织,不要什么都往一张表里塞。我们短剧出海的DWS层按三个主题建设:用户行为主题、广告投放主题、内容消费主题。

第二,粒度层次要清晰。同一个主题可能有多个粒度的汇总表,命名上要区分开:

dws_user_behavior_1d     -- 用户+天 粒度(每天一行)
dws_user_behavior_7d     -- 用户+周 粒度(最近7天汇总)
dws_user_behavior_30d    -- 用户+月 粒度(最近30天汇总)

第三,DWS层的表是最适合建宽表的位置。把一个主题下常用的维度和指标都聚合进来,下游直接查。

ADS层:应用数据层

ADS的全称是Application Data Store,应用数据层。 image.png

这层是数仓的"出口",直接对接报表平台、BI大屏、数据API、推荐系统等。

ADS层的特点是高度定制化,每张表通常只为一个特定的报表或接口服务。

-- 典型ADS表:每日经营日报
CREATE TABLE ads_daily_business_report AS
SELECT
    dt,
    COUNT(DISTINCT user_id)                          AS dau,
    COUNT(DISTINCT new_user_id)                      AS new_users,
    SUM(revenue)                                     AS total_revenue,
    SUM(ad_cost)                                     AS total_cost,
    ROUND(SUM(revenue) / SUM(ad_cost), 2)            AS roi,
    ROUND(SUM(revenue) / COUNT(DISTINCT user_id), 2) AS arpu
FROM dws_user_revenue_1d
WHERE dt = '${bizdate}'
GROUP BY dt;

ADS层通常表不多但使用频率最高,查询性能要求也最高。很多公司会把ADS层的数据从Hive/Spark搬到OLAP引擎(Doris、ClickHouse、Hologres)里,支持秒级查询响应。


面试怎么答才能拿高分?

image.png

低分回答(60分)

「数仓分四层,ODS原始层、DWD明细层、DWS汇总层、ADS应用层。」

面试官内心OS:这你是背的吧,谁都知道。 image.png

中等回答(75分)

「ODS保留原始数据不做处理,DWD做清洗标准化和维度关联,DWS按主题预聚合,ADS直接给报表用。分层的目的是以空间换时间,让数据处理可复用。」

面试官内心OS:概念理解到位了,但不知道你有没有实际做过。

高分回答(90分)

「我们公司的数仓是标准四层架构。ODS层接入了MySQL业务库、埋点日志和Facebook/TikTok广告API三类数据源,大概20多张表。DWD层我负责的是广告域和用户域,大概15张明细表,每天处理的数据量在3000万条左右。其中最大的挑战是跨时区处理——我们是出海业务,用户遍布多个时区,DWD层要把UTC时间转成用户当地时间,不然留存率、日活这些指标全是错的。DWS层按用户行为、广告投放、内容消费三个主题建设,ADS层的数据推到Hologres供报表平台查询。」

面试官内心OS:这人真做过,细节都能说清楚。

高分回答的关键: 有表数量、有数据量、有你负责的部分、有你遇到的具体问题。这些具体的细节是背不出来的,只有实际做过才能说得出来。 image.png


追问防线:面试官常见追问

面试不会只问一个问题就结束。当你回答完分层架构后,面试官大概率会追问以下问题:

追问1:DWD层和ODS层有什么区别?我能不能跳过DWD直接从ODS到DWS? image.png

技术上当然可以跳过,但不建议。ODS层的数据质量是不可控的——NULL值、格式不统一、重复数据都有。如果跳过DWD直接聚合,这些脏数据会一路"污染"到下游所有报表。DWD层就是那道"防火墙",保证数据质量有一个统一的出口。

另外,DWD层做的维度关联和标准化,是多个DWS表共享的。跳过DWD意味着每个DWS表都要自己做一遍清洗,重复劳动巨大。

追问2:你们有没有遇到过分层不合理的情况? image.png

有。之前有同事为了赶进度,把一些聚合逻辑直接写在了DWD层(比如在DWD里就按天聚合了),导致DWD层丢失了明细粒度,后来业务方要查单笔订单详情的时候,DWD层查不到,只能回ODS重新处理。

从那以后我们定了一个规矩:DWD层只做清洗和关联,不做聚合。聚合是DWS的事。

追问3:分层之间的依赖关系怎么管理? image.png

通过调度系统来管理。我们用的是DataWorks(阿里云的调度平台),DAG上配好依赖关系:ODS任务跑完才触发DWD,DWD跑完才触发DWS,DWS跑完才触发ADS。任何一层失败会阻塞下游,同时发告警通知。


总结

image.png

  • 分层的本质是以空间换时间,让数据处理可复用、可维护
  • ODS:原始数据不动,留底用于回溯
  • DWD:清洗+标准化+维度关联,是数仓质量的防火墙(最核心)
  • DWS:按主题预聚合,减少下游重复计算
  • ADS:直接给应用层使用,高度定制化
  • 面试时能说出具体数字和你负责的部分,比背概念强10倍
刷题进度保存在当前浏览器