Просмотр исходного кода

docs(kb/33): 加 TDM 建模文档(EAV 7 字段不扩 + _d 日更 / _o 凝固分表 + 71 + 32 个 tag_code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tianyu.chu 2 дней назад
Родитель
Сommit
c127d67aec
3 измененных файлов с 204 добавлено и 0 удалено
  1. 1 0
      README.md
  2. 202 0
      kb/33-tdm建模.md
  3. 1 0
      kb/92-重构进度.md

+ 1 - 0
README.md

@@ -92,6 +92,7 @@ PG/ES ──DataX(raw)──> RAW ──> ODS ──> DWD ──> DWS ──> TD
 | [27-dwd 建模](kb/27-dwd建模.md) | **DWD 字段建模**:业务过程拆分 + 维度退化策略 + 单分区不回算 + 各业务过程字段表 |
 | [28-dim 建模](kb/28-dim建模.md) | **DIM 字段建模**:ful_d 优先选型 + ful→zip 触发条件 + 跑批策略(初始化 + 增量合并)+ 脏数据清洗位置 + 各实体字段表 |
 | [29-dws 建模](kb/29-dws建模.md) | **DWS 字段建模**:日聚合主题宽表单一职责 + 维度退化触发条件 + N=2 回算(与 DWD 对齐)+ dws_usr_user_trade_1d 字段表 |
+| [33-tdm 建模](kb/33-tdm建模.md) | **TDM 字段建模**:EAV 7 字段不扩 + _d 日更 / _o 一次性凝固分表 + tag_code 命名约定 + 16 品类 × 4 窗口偏好 + 7 属性 + 跨年扩张规则 |
 
 ### 3x 开发流程
 

+ 202 - 0
kb/33-tdm建模.md

@@ -0,0 +1,202 @@
+# TDM 建模
+
+> 本数仓 TDM 层(主题域模型 / 画像层)的字段建模与设计约定。建模方法论见 `20-数仓分层与建模.md` §5;标签体系总览见 `23-标签体系.md`;命名规则见 `21-命名规范.md`;时间语义见 `26-时间语义.md`。
+>
+> 本文档按"表一节"组织,每节包含表用途 / 粒度 / 来源 / tag_code 清单 / 写入策略。
+
+## 1. 通用约定
+
+### 1.1 框架字段
+
+所有 TDM 表必带 `etl_time TIMESTAMP` + 分区 `dt STRING`,`STORED AS ORC`。
+
+### 1.2 EAV 长表 schema(kb/23 §2 严守 7 字段)
+
+所有标签表共用 EAV 7 字段,**不扩展**:
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| entity_id | BIGINT | 实体 id(用户场景 = user_id)|
+| tag_code | STRING | 标签编码(维度全 encode 到此字段,命名约定见 §4)|
+| tag_value | STRING | 标签值(统一 STRING;数值标签如 `"1500.00"`,下游 CAST 处理)|
+| tag_type | STRING | `attr` / `stat` / `rule`(预留枚举值 `algo` 给机器学习标签)|
+| confidence | DECIMAL(5,4) | 置信度(规则标签 1.0;模型标签按模型输出)|
+| etl_time | TIMESTAMP | ETL 处理时间 |
+| dt | STRING | 分区,按表语义取(见各表节)|
+
+**收益**:schema 不绑定标签维度组合;新增维度(如 channel / device 等)只需新 tag_code 命名,**不需 ALTER**。
+
+**代价**:维度切片需 `tag_code LIKE 'usr_pref_trade_%_amt_30d'` 模式匹配;数值标签聚合 / 排序需 `CAST(tag_value AS DECIMAL)`。
+
+### 1.3 表分类:_d 日更 / _o 一次性凝固
+
+按更新周期拆表(业界主流 OneData / 字节 / 美团做法):
+
+| 后缀 | 写入策略 | 含哪些标签 |
+|---|---|---|
+| `_d` | 每天 INSERT OVERWRITE 单分区 dt=T-1 | 属性 snap + 偏好滚动窗口(30d)+ 偏好当年累计(y{当年})|
+| `_o` | 一次性写入永远不动 | 偏好往年总额(y{往年},跨年凝固时新建一张)|
+
+**为什么分两类**:
+
+- y{往年} 一旦凝固永不变,与 _d 日更"每天重刷 T-1"语义冲突 — 混在 _d 表会让"永远不变"的标签被天天扫一遍重写
+- 各自独立调度:_d 是日 DAG,_o 是手动 once(落 `manual/backfill/`,跑一次写完)
+
+### 1.4 写入策略
+
+| 表 | dt 锚点 | 写入 |
+|---|---|---|
+| `_d` | T-1(`${dt}`)| `INSERT OVERWRITE TABLE ... PARTITION (dt='${dt}')` 静态单分区 |
+| `_o` | 凝固年 12-31(如 `20251231`)| `INSERT OVERWRITE TABLE ... PARTITION (dt='20251231')` 静态单分区 |
+
+### 1.5 调度依赖
+
+- `_d` sche:DS DEPENDENT 同 dt `dim_usr_user_ful_d`(属性源)+ 同 dt `dws_usr_user_trade_1d`(偏好源)
+- `_o`:不挂日调度,手跑 once
+
+---
+
+## 2. tdm_usr_tag_d(用户标签 日更长表)
+
+### 2.1 用途
+
+承载 1 期用户标签的日更部分:7 个属性 + 16 品类 × 4 偏好窗口(30d 金额/次数 + y{当年} 金额/次数)。
+
+### 2.2 粒度
+
+每个 (entity_id, tag_code) 在 dt 分区内唯一。
+
+### 2.3 来源
+
+- 属性 7 个 ← `dim_usr_user_ful_d WHERE dt='${dt}'`
+- 偏好 30d 金额/次数 ← `dws_usr_user_trade_1d WHERE dt BETWEEN $[yyyyMMdd-30] AND ${dt}` 滚动聚合
+- 偏好 y{当年} 金额/次数 ← `dws_usr_user_trade_1d WHERE dt BETWEEN '{当年}0101' AND ${dt}` 滚动覆盖(1 期当年 = 2026)
+
+### 2.4 tag_code 清单
+
+#### 属性类(tag_type=attr,7 个)
+
+| tag_code | 描述 | 来源 / 算法 | tag_value 格式 |
+|---|---|---|---|
+| `usr_level` | 用户等级 | dim_usr_user.member_level | 数字字符串(如 `"3"`)|
+| `usr_is_cert` | 实名认证情况 | dim_usr_user.is_cert | `"true"` / `"false"` |
+| `usr_sex` | 性别 | dim_usr_user.sex_cert(未实名 NULL)| `"M"` / `"F"` |
+| `usr_city` | 城市 | dim_usr_user.cert_city(未实名 NULL)| 城市名 |
+| `usr_register_time` | 注册时间 | dim_usr_user.reg_create_time | `yyyyMMdd` |
+| `usr_birth_month` | 生日年月 | dim_usr_user.birthday_cert 派生(未实名 NULL)| `yyyyMM` |
+| `usr_generation` | 出生世代 | dim_usr_user.birthday_cert 派生(按 §6 切片)| 世代名("90后" / "00后" 等)|
+
+#### 偏好类(tag_type=stat,16 品类 × 4 窗口 = 64 个)
+
+| tag_code 模板 | 描述 | 来源 |
+|---|---|---|
+| `usr_pref_trade_{category}_amt_30d` | 某品类近 30 天金额 | `SUM(pay_amt_cny)` WHERE dt IN [T-30, T-1] |
+| `usr_pref_trade_{category}_cnt_30d` | 某品类近 30 天次数 | `SUM(pay_order_cnt)` 同口径 |
+| `usr_pref_trade_{category}_amt_y2026` | 某品类 26 年累计金额 | `SUM(pay_amt_cny)` WHERE dt IN [20260101, T-1](每天滚动覆盖)|
+| `usr_pref_trade_{category}_cnt_y2026` | 某品类 26 年累计次数 | `SUM(pay_order_cnt)` 同口径 |
+
+`{category}` 是 16 品类占位(kb/28 §3.2 DIM 已清洗):Basketball / Soccer / MLB / TCG / PTCG / 影视收藏 / 综合体育 / 综合收藏 / NFL / NHL / UFC / WWE / Tennis / Esports / F1 / Golf。
+
+**金额口径**:`SUM(pay_amt_cny)` = Net Revenue 偏好金额(A3 锁 1 期不扣减退款;GMV = `payable_amt_cny`,偏好 amt 用 pay_amt_cny,见 kb/27 §2.5 / kb/29 §2.5)。
+
+**次数口径**:`SUM(pay_order_cnt)` = `COUNT(DISTINCT order_id)`(A2 锁定,1 期不用份数)。
+
+### 2.5 字段表
+
+按 EAV 7 字段(同 §1.2),dt = `${dt}` = T-1。
+
+### 2.6 行数估算
+
+- 属性:30 万用户 × 7 = 210 万行
+- 偏好:30 万用户 × ~5 活跃品类 × 4 窗口 = 600 万行
+- 合计 ~ 800 万行 / 分区,ORC 压缩后 < 200 MB
+
+---
+
+## 3. tdm_usr_tag_y2025_o(25 年总额标签 一次性凝固)
+
+### 3.1 用途
+
+承载 25 年总额标签(16 品类 × 金额 / 次数 = 32 个),开发完一次性跑入永远不动。
+
+### 3.2 粒度
+
+每个 (entity_id, tag_code) 在 dt=20251231 分区内唯一。
+
+### 3.3 来源
+
+`dws_usr_user_trade_1d WHERE dt BETWEEN '20250101' AND '20251231'` 一次性聚合。
+
+### 3.4 tag_code 清单(tag_type=stat,32 个)
+
+| tag_code 模板 | 描述 | 来源 |
+|---|---|---|
+| `usr_pref_trade_{category}_amt_y2025` | 某品类 25 年总额 | `SUM(pay_amt_cny)` WHERE dt IN [20250101, 20251231] |
+| `usr_pref_trade_{category}_cnt_y2025` | 某品类 25 年总次数 | `SUM(pay_order_cnt)` 同口径 |
+
+`{category}` 同 §2.4 16 品类。
+
+### 3.5 dt 锚点 + 写入
+
+- `dt='20251231'` 永久固定
+- 跑一次写入后永远不动
+- 落 `manual/backfill/{yyyymmdd}_tdm_usr_tag_y2025_o.sql` 一次性 SQL,不挂日调度
+
+### 3.6 行数估算
+
+30 万用户 × ~5 活跃品类 × 2 度量 ≈ 300 万行(永久),ORC 压缩后 < 100 MB。
+
+---
+
+## 4. tag_code 命名约定
+
+| 类型 | 模板 | 示例 |
+|---|---|---|
+| 属性 | `usr_{属性名}` | `usr_sex` / `usr_city` / `usr_generation` |
+| 偏好 | `usr_pref_{主题}_{category}_{metric}_{window}` | `usr_pref_trade_basketball_amt_30d` / `usr_pref_trade_mlb_cnt_y2025` |
+
+**所有维度(品类 / 窗口 / 度量)全部 encode 到 tag_code**。下游按维度切片用 `tag_code LIKE 'usr_pref_trade_%_amt_30d'` 等模式匹配。
+
+`{window}` 取值约定:
+
+- `snap`(当前快照,T-1 的属性)
+- `30d`(近 30 天滚动 [T-30, T-1])
+- `y{yyyy}`(按年累计 / 总额;当年 = _d 表滚动,往年 = _o 表凝固)
+
+---
+
+## 5. 跨年扩张规则
+
+每年 1-1 凝固上一年累计为新表:
+
+| 时点 | 动作 |
+|---|---|
+| 2027-01-01 | 新建 `tdm_usr_tag_y2026_o`(凝固 26 年累计);`tdm_usr_tag_d` 移除 y2026 偏好 tag_code + 新增 y2027 tag_code |
+| 2028-01-01 | 新建 `tdm_usr_tag_y2027_o`;同理 |
+
+`_o` 表数据永久保留。下游想看历年总额按 `dt='{凝固年}1231'` 查对应 `_o` 表。
+
+---
+
+## 6. 属性细节默认口径(待业务回头校准)
+
+按 tdm.md 7 个属性,业务侧未给精确口径前先用合理默认(源字段已在 kb/28 §2 确认);调字段不动 schema(EAV 收益)。
+
+| tag_code | 默认口径 | 待业务确认 |
+|---|---|---|
+| `usr_level` | `member_level` | 业务库另有 `level` 字段,两字段语义区别待澄清;1 期取 member_level |
+| `usr_is_cert` | `is_cert` 布尔 → `"true"` / `"false"` | 是否区分"未实名 / 审核中 / 已实名"3 态?1 期 2 态 |
+| `usr_sex` | `sex_cert`(M / F),未实名 NULL | 未实名是否回退取自填 sex?1 期 cert 一源 |
+| `usr_city` | `cert_city`,未实名 NULL | 未实名是否走 login_addr / register_addr?1 期 cert_city 一源 |
+| `usr_register_time` | `reg_create_time` → `DATE_FORMAT 'yyyyMMdd'` | 是否要月 / 周粒度?1 期日粒度 |
+| `usr_birth_month` | `birthday_cert` → `DATE_FORMAT 'yyyyMM'`,未实名 NULL | 是否只取月(MM)?1 期 yyyyMM 留年信息 |
+| `usr_generation` | `birthday_cert` 派生 10 年切片:60 前 / 60 后 / 70 后 / 80 后 / 85 后 / 90 后 / 95 后 / 00 后 / 05 后 / 10 后 | 切片粒度(5 年 / 10 年)?切片名(中文"90后" / 英文"1990s")?1 期中文 N 后 |
+
+---
+
+## 7. 不在 1 期 scope
+
+- **TDM 宽表** `tdm_usr_profile_ful_d`:1 期标签未完善(业界规范长表稳定后再做宽表,见 kb/23 §3.2),2 期再评估
+- **TDM 人群包** `tdm_usr_crowd_ful_d`:1 期不做圈选
+- **商品标签 / 店铺标签**:tdm.md mermaid 图的 C / D 分支,1 期 scope 外
+- **algo 类标签**:`tag_type` 预留枚举值,1 期无实际数据;将来接 RFM / 用户生命周期预测等 ML 标签时启用

+ 1 - 0
kb/92-重构进度.md

@@ -183,6 +183,7 @@
 | 2026-05-09 | **kb/27 discount→merchant_discount + discount_amount 加派生 + DWD 调度双前置**:(a) §2.5 派生映射 `discount` 加 merchant_ 前缀(`discount_amt_cny` → `merchant_discount_amt_cny`):业务库字段 `discount` 字面"折扣"未体现"商家方"语义,加全名 merchant_ 前缀突出商家维度(不抄业务侧 SQL 别名 mer_coupon,因公司"折扣 vs 券"是否同义未确认,待业务侧定)。(b) `discount_amount` 从"待问是否派生"拍板按通式派生,临时命名 `discount_amount_amt_cny ❓`,与 merchant_discount 语义区别 + 最终命名仍待业务答复。(c) §1.4 加调度依赖行:DS DEPENDENT 同 dt ODS + 同 dt DIM ful_d 双前置(按 Kimball 维度退化原则,DWD 必依赖 DIM 同分区跑完)。 | — |
 | 2026-05-10 | **kb/93 加 ADR-08 DIM ful_d 跑批:业界主流模式 B + 字段 CASE WHEN 整组判断**:本项目第一张 dim 表 dim_usr_user_ful_d 落地(commits 5a28815 / 305a63b / ad7925a / 5d09add / 7c6cb5e)后沉淀通用范式:(a) init 扫 ods 历史 dt<${dt} + ROW_NUMBER 取每 pk 最新版本落 dim.${pdt} 首日分区;(b) sche 今日 ods 增量 + 昨日 dim 基线 + UNION ALL(today_rebuilt 重 join + unchanged 直接保留);(c) 字段合并按"组"用 CASE WHEN(如 base 整组按 bu.id IS NOT NULL,cert 整组按 ci.user_id IS NOT NULL),不用字段级 COALESCE 避免业务库主动置 NULL 场景下昨日值错误兜底;(d) init 与 sche 同日上线 init 灌 ${pdt} + sche 写 ${dt} 链路打通。否决方案:每日全量重算(扫描成本高)/ FULL JOIN + COALESCE(NULL 不安全)/ 扫 ods 历史 + broadcast filter(IO 成本高)/ MERGE INTO(Spark 2.4 不支持,迁 Spark 3+ / Hudi / Iceberg / Delta Lake 后再用)。 | — |
 | 2026-05-10 | **dwd_pay sche 改回算近 2 日 + 删 order_type='group' 过滤**:(a) 删 init / sche 两处 `order_type='group'`:dwd_pay 是数仓支付明细不限定订单类型,原 SQL 把 temp.sql 业务侧"拼团 GMV"分析的 `order_type='group'` 限定误搬入 dwd 入仓 SQL。(b) sche 写入策略从"单分区 dt=T-1 不回算"改为"回算近 2 日(dt IN ${dt}/${pdt})":原"不回算"基于"业务库 update_time 与 payment_success_time 同步刷新 OLTP 契约"假设;用户拍板"后端不可信",按业界主流 N=2 兜底跨零点漂移(业务时间 T-1 但 update_time 漂到 T 的事件,T+1 跑批时通过扫 ods.dt=T 兜回)。INSERT OVERWRITE PARTITION (dt) 动态分区只覆盖 SELECT 出现的 dt(kb/26 §8 项目默认 DYNAMIC mode)。(c) kb/27 §1.4 写入策略段同步更新(原"不回算"改"回算近 2 日 N=2 业界主流")。本次反复经过:5/9 信契约不回算 → 5/10 编漂移场景反悔 → 复盘 sessions 5/9 T8-T10 发现"自破契约"违反共识 → 用户重新拍板"后端不可信"接受回算 → 业界范围 N=2/3 拍 N=2。 | — |
+| 2026-05-11 | **kb/33-tdm建模.md 新建**:1 期用户标签 EAV 长表建模(属性 7 + 偏好 16 品类 × 4 窗口 = 64 共 71 个 tag_code)。设计要点:(a) 严守 kb/23 §2 EAV 7 字段不扩(entity_id / tag_code / tag_value / tag_type / confidence / etl_time / dt),新增维度组合只动 tag_code 命名不 ALTER 表;(b) tag_type 枚举 `attr / stat / rule`,预留 `algo` 给将来机器学习标签;(c) 按更新周期拆 `_d` 日更(属性 snap + 30d 滚动 + y{当年} 当年累计)/ `_o` 一次性凝固(y{往年} 总额,跨年 1-1 新建)两类表,业界 OneData / 字节做法;(d) tag_code 命名 `usr_{属性名}` / `usr_pref_{主题}_{category}_{metric}_{window}`,所有维度 encode 到 tag_code 字段;(e) tdm_usr_tag_d 来源 dim_usr_user_ful_d(属性)+ dws_usr_user_trade_1d(偏好);tdm_usr_tag_y2025_o 一次性聚合 dws_1d [20250101, 20251231];(f) 7 属性细节口径用合理默认(usr_level = member_level / usr_sex = sex_cert / usr_city = cert_city / usr_register_time = yyyyMMdd / usr_birth_month = yyyyMM / usr_generation = 10 年切片中文 N 后 / usr_is_cert = 布尔),待业务侧回头校准(EAV 收益);(g) 跨年扩张规则:每年 1-1 凝固上年累计为新 _o 表 + _d 表移除 y{上年} 新增 y{当年} tag_code。1 期 scope 外:宽表 / 人群包 / 商品店铺标签 / algo 类标签。README §2x 数仓建模索引加 kb/33 行。 | — |
 | 2026-05-10 | **kb/29-dws建模.md 新建**:1 期 DWS 单张主题宽表 `dws_usr_user_trade_1d`(用户 × 品类 × 日 交易聚合,pay 单源),承载偏好标签金额 / 次数聚合基础。设计要点:(a) DWS 单一职责日聚合,不爆窗口表(30d / yYYYY 由 TDM 跑批从 dws_1d 滚动聚合);(b) 1 期 scope 不冗余维度退化字段(仅服务标签计算),触发条件 = 上层 BI 直查 / 多维度组合 / 跨域 join 频繁,补法 = ALTER ADD COLUMN 重刷历史分区或新建第二张主题宽表(业界 1-3 张/主题);(c) 回算窗口 N=2 与 DWD 对齐(漂移连锁补偿,参 ADR-09);(d) 度量保留 13 列(pay_order_cnt + purchase_cnt + payable/pay/trade/settle/4 类 discount/point_deduct/shipping + point),砍 7 列(card_price / act_price / discount_amount_amt_cny ❓ / shipping_free / discount_point / give_cnt);(e) refund / net 度量 1 期 N/A,2 期接退款时按业务诉求选择"补 refund 列 + net 冗余"或"单独 refund 主题宽表";(f) 字段命名按业界全名 + `_amt_cny` 后缀,与 kb/27 对齐。README §2x 数仓建模索引加 kb/29 行。 | — |
 | 2026-05-10 | **kb/93 加 ADR-09 DWD 事件表跑批回算窗口 N=2**:本项目第一张 dwd 事件表 dwd_trd_order_pay_apd_d 落地反复讨论后(5/9 信契约不回算 → 5/10 编漂移场景 → 自破契约 → 拍 N=2,commits 5a28815/a9b6eaa)定下范式。决策:sched=T 时扫 ods.dt IN (${dt},${pdt}) + 过滤业务时间 + INSERT OVERWRITE PARTITION (dt) 动态分区写 dwd.dt IN (${dt},${pdt});T+1 跑批时通过扫 ods.dt=T 兜回业务时间 T-1 漂移到 ods.dt=T 的事件重写 dwd.dt=T-1。理由:ods 已按 update_time 严格归位但业务库 OLTP 业务事件时间 / update_time 不一定严格同步(跨零点支付 600ms 延迟漂移到 ods.dt=T 单分区扫不到)。N=2 业界主流(阿里 OneData / 字节 / 美团默认)。否决:N=1 不回算(信契约不安全)/ N=3 保守 / N=7 极端 / MERGE INTO(Spark 2.4 不支持)/ 延后 1 天跑批(数据延迟不可接受)。反悔:业务库延迟 > 1 天频繁→上调 N / 迁 Spark 3+ → MERGE INTO / OLTP 强契约保证→降 N=1(实际很难保证)。 | — |
 | 2026-04-22 | **仓库改名 `tendata-warehouse-release` → `poyee-data-warehouse` 收尾**:项目根目录由用户手动改名完成;代码侧 `dw_base/utils/file_utils.py:9` + `dw_base/utils/hdfs_merge_small_file.py:7` 两处 `re.sub(r"tendata-warehouse.*", ...)` 字面量同步更新。`.idea/*.iml` / `modules.xml` / `workspace.xml` 因 `.idea` + `*.iml` 在 `.gitignore`,属本地 IDE 状态,不入库亦不影响运行(老 `tendata-warehouse-release.iml` + modules.xml / workspace.xml 里的 module name 残留不处理)。联动 kb/90 §1.1 L88 表格行打勾 + §2.3 末尾"与仓库改名的联动"段压缩为一行记录 | — |