画像项目的数仓分层怎么设计?ODS→DWD→DWS→ADS每层放什么?
先说结论
画像项目的数仓分层和通用数仓一样是四层,但每层的侧重点不同:ODS存原始日志、DWD做数据清洗和ID打通、DWS做行为指标汇总、ADS输出标签宽表。
画像分层的特殊之处在于DWS层。通用数仓的DWS是按主题域做汇总(订单域、用户域),画像数仓的DWS是按标签计算维度做汇总(用户日粒度行为、用户周粒度消费)——本质是为标签加工做中间层。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
数仓分层是大数据面试的经典考点,但结合画像项目来讲分层,很多人讲不好。面试官不只是想听你背ODS/DWD/DWS/ADS的定义,更想听你在画像项目中每层具体放了什么表、每层的加工逻辑是什么、层与层之间的依赖关系怎么设计。这道题考的是你的数仓建模能力和架构设计思维。
画像数仓四层架构全景
画像数仓分层架构:
ODS(原始数据层) 每天增量采集
ods_app_event -- App埋点事件日志
ods_order_log -- 订单流水
ods_user_register -- 用户注册信息
ods_device_binddata -- 设备绑定数据
ods_push_log -- 推送日志
|
v (清洗、去重、ID打通)
DWD(明细数据层) T+1更新
dwd_user_event -- 清洗后的行为事件(去重+过滤测试数据)
dwd_order_detail -- 清洗后的订单明细(币种统一+退款处理)
dwd_user_register_clean -- 清洗后的注册信息
dim_oneid_mapping -- OneID映射表
dim_user_base -- 用户基础信息维度表
|
v (聚合计算各维度指标)
DWS(汇总数据层) T+1更新
dws_user_active_1d -- 用户每日活跃汇总
dws_user_order_1d -- 用户每日订单汇总
dws_user_active_7d -- 用户近7天活跃汇总
dws_user_order_30d -- 用户近30天订单汇总
|
v (标签计算 + 输出宽表)
ADS(应用数据层) T+1更新
ads_user_profile -- 用户画像宽表(最终输出)
ads_user_tag_kv -- 标签KV表(灵活扩展)
ads_user_segment -- 用户分群结果表
DWD层:数据清洗三板斧
DWD层是画像质量的第一道关卡。清洗不到位,后面的标签全是错的。
-- DWD层清洗示例:行为事件清洗
INSERT OVERWRITE TABLE dwd_user_event PARTITION (dt='${bizdate}')
SELECT
COALESCE(m.one_id, a.user_id) AS user_id, -- ID打通:优先用OneID
a.event_name,
a.event_time,
a.device_id,
a.country,
a.platform -- iOS/Android
FROM ods_app_event a
LEFT JOIN dim_oneid_mapping m ON a.device_id = m.device_id
WHERE a.dt = '${bizdate}'
AND a.user_id IS NOT NULL -- 清洗1:过滤未登录用户
AND a.event_time IS NOT NULL -- 清洗2:过滤时间为空的脏数据
AND a.device_id NOT IN ( -- 清洗3:过滤测试设备
SELECT device_id FROM dim_device_blacklist
)
AND a.event_name NOT LIKE 'test_%'; -- 清洗4:过滤测试事件
DWD层的核心清洗动作:①ID打通(关联OneID映射表);②过滤脏数据(NULL、空值、异常时间戳);③过滤测试数据(测试设备、测试事件);④去重(同一事件可能因网络重试上报多次);⑤字段标准化(币种统一、时区统一)。
DWS层:为标签计算做中间准备
DWS层是画像数仓的关键层。它把DWD的明细数据按不同时间窗口和维度做聚合,为ADS层的标签计算提供中间结果。
-- DWS层示例:用户近7天活跃汇总
INSERT OVERWRITE TABLE dws_user_active_7d PARTITION (dt='${bizdate}')
SELECT
user_id,
COUNT(*) AS event_cnt_7d, -- 近7天总事件数
COUNT(DISTINCT DATE(event_time)) AS active_days_7d, -- 近7天活跃天数
COUNT(DISTINCT event_name) AS event_types_7d, -- 近7天使用的功能数
MAX(event_time) AS last_active_time, -- 最近活跃时间
MIN(event_time) AS first_active_time_7d, -- 7天内首次活跃时间
SUM(CASE WHEN event_name = 'purchase' THEN 1 ELSE 0 END) AS purchase_cnt_7d
FROM dwd_user_event
WHERE dt BETWEEN DATE_SUB('${bizdate}', 6) AND '${bizdate}'
GROUP BY user_id;
-- 类似地还有:dws_user_active_30d, dws_user_order_7d, dws_user_order_30d
-- 不同时间窗口的汇总表,给ADS层不同标签使用
为什么要在DWS层做中间汇总,不直接在ADS层从DWD计算标签?两个原因:①复用——多个标签可能都需要「近7天活跃天数」这个中间结果,DWS算一次,ADS复用多次,避免重复计算;②性能——DWD是明细数据(几亿行),每个标签SQL都从DWD开始算太慢,DWS先聚合到用户粒度(千万行),ADS层计算就快很多。
ADS层:标签计算和宽表输出
-- ADS层:最终用户画像宽表
INSERT OVERWRITE TABLE ads_user_profile PARTITION (dt='${bizdate}')
SELECT
u.user_id,
-- 基础属性标签(来自DIM维度表)
u.gender, u.age_group, u.country, u.register_date,
-- 活跃类标签(来自DWS活跃汇总)
a7.active_days_7d, a7.event_cnt_7d,
a30.active_days_30d, a30.login_cnt_30d,
-- 消费类标签(来自DWS订单汇总)
o30.order_cnt_30d, o30.total_amount_30d,
-- 计算标签(基于DWS中间结果计算)
CASE WHEN o30.total_amount_30d >= 150 AND o30.order_cnt_30d >= 3 THEN 'high'
WHEN o30.total_amount_30d >= 50 THEN 'medium'
ELSE 'low' END AS value_level,
CASE WHEN DATEDIFF('${bizdate}', u.register_date) <= 7 THEN 'new_user'
WHEN a7.active_days_7d IS NULL OR a7.active_days_7d = 0 THEN
CASE WHEN a30.active_days_30d > 0 THEN 'silent' ELSE 'churned' END
WHEN a30.login_cnt_30d >= 15 AND o30.order_cnt_30d > 0 THEN 'mature'
ELSE 'active' END AS lifecycle_stage
FROM dim_user_base u
LEFT JOIN dws_user_active_7d a7 ON u.user_id = a7.user_id AND a7.dt = '${bizdate}'
LEFT JOIN dws_user_active_30d a30 ON u.user_id = a30.user_id AND a30.dt = '${bizdate}'
LEFT JOIN dws_user_order_30d o30 ON u.user_id = o30.user_id AND o30.dt = '${bizdate}';
面试高频追问
追问一:DWS层做了7天和30天的汇总,为什么不直接做一张全时间窗口的表?
因为不同时间窗口的更新频率不同。7天汇总每天要重算(因为窗口在滑动),30天汇总也要每天重算。如果合成一张表,每天要重算所有窗口的指标,计算量大且不灵活。分开存储后,如果某天只有7天活跃汇总出了问题,只需要重跑dws_user_active_7d,不影响30天的汇总。独立分层也让数据血缘更清晰——出了问题能快速定位是哪一层出的。
追问二:DIM维度表和DWD放在同一层合理吗?
严格来说DIM是独立的维度层,不属于DWD。但在画像项目中,维度表通常比较简单(用户基础信息表、设备信息表),我习惯把DIM表的加工逻辑放在DWD层的调度DAG里一起管理。如果维度表比较复杂(比如需要做SCD Type2拉链),可以单独作为DIM层管理。面试时你只要说清楚DIM表的来源和更新策略就行,放在哪一层不是关键问题。
追问三:画像宽表的JOIN怎么保证性能?LEFT JOIN太多了会不会慢?
ADS层那个SQL有4个LEFT JOIN,确实可能慢。优化方法:①确保JOIN Key(user_id)没有数据倾斜;②DWS表已经做了聚合,数据量从亿级降到千万级,JOIN不会太慢;③如果还是慢,可以把多个JOIN改成分步执行——先JOIN活跃表,结果写临时表,再JOIN订单表。分步执行虽然多了IO,但每步的Shuffle数据量小了,总体更快。我在出海硬件品牌的画像宽表每天跑约15分钟(千万级用户 x 4个LEFT JOIN),性能可以接受。
实战踩坑
我在某短剧出海公司做画像数仓时踩过一个坑:一开始没有DWS层,ADS标签直接从DWD明细数据计算。120个标签每个都要扫描DWD行为表(每天几亿行),导致凌晨的计算任务排队严重,整个标签链路要跑3个多小时。
后来加了DWS层后,先做一次聚合(几亿行→千万行),120个标签都从DWS表取数据。总计算时间从3小时降到45分钟——DWS聚合花15分钟,ADS标签计算花30分钟。
教训:DWS层不是可有可无的,它是画像计算性能的关键。没有DWS,每个标签SQL都要扫描原始明细数据,计算量爆炸。有了DWS,明细数据只扫描一次做聚合,后续标签计算都基于聚合结果,又快又省资源。
总结
画像数仓分四层:ODS存原始→DWD清洗去重+ID打通→DWS按时间窗口聚合→ADS输出标签宽表。DWS是画像分层的关键——一次聚合、多次复用,避免每个标签都重复扫描明细数据。面试时能画出四层的表和依赖关系,讲清楚每层的核心加工逻辑,比背定义有说服力。
下一篇聊一个合规类话题:GDPR/隐私合规——用户画像怎么做数据脱敏和权限控制?
你们画像数仓有DWS层吗?加了中间层后性能提升了多少?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长