订阅更新

画像项目的数仓分层怎么设计?ODS→DWD→DWS→ADS每层放什么?

用户画像手册 第 18 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. 画像数仓四层架构全景
  3. DWD层:数据清洗三板斧
  4. DWS层:为标签计算做中间准备
  5. ADS层:标签计算和宽表输出
  6. 面试高频追问
  7. 实战踩坑
  8. 总结

先说结论

画像项目的数仓分层和通用数仓一样是四层,但每层的侧重点不同: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年大数据老兵,聊技术、聊职场、聊成长

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