瀏覽代碼

docs(kb/33): tdm 往年凝固改方案 B(单表 tdm_usr_tag_o + 多 dt 分区)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tianyu.chu 2 天之前
父節點
當前提交
e22c1dd095
共有 2 個文件被更改,包括 18 次插入16 次删除
  1. 17 16
      kb/33-tdm建模.md
  2. 1 0
      kb/92-重构进度.md

+ 17 - 16
kb/33-tdm建模.md

@@ -113,38 +113,39 @@
 
 ---
 
-## 3. tdm_usr_tag_y2025_o(25 年总额标签 一次性凝固)
+## 3. tdm_usr_tag_o(往年总额标签 一次性凝固)
 
 ### 3.1 用途
 
-承载 25 年总额标签(16 品类 × 金额 / 次数 = 32 个),开发完一次性跑入永远不动
+承载历年(往年)总额标签长表。1 期落地 25 年凝固(16 品类 × 金额 / 次数 = 32 个 tag_code),开发完一次性跑入永远不动;后续每年 1-1 凝固上一年累计新落 dt 分区,**不分新表**
 
 ### 3.2 粒度
 
-每个 (entity_id, tag_code) 在 dt=20251231 分区内唯一
+每个 (entity_id, tag_code) 在 dt 分区内唯一。每个 dt 分区对应一个凝固年(25 年 → `dt='20251231'`,26 年 → `dt='20261231'`,以此类推)
 
 ### 3.3 来源
 
-`dws_usr_user_trade_1d WHERE dt BETWEEN '20250101' AND '20251231'` 一次性聚合
+`dws_usr_user_trade_1d WHERE dt BETWEEN '{凝固年}0101' AND '{凝固年}1231'` 一次性聚合。1 期凝固 25 年用 `[20250101, 20251231]`
 
-### 3.4 tag_code 清单(tag_type=stat,32 个)
+### 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)` 同口径 |
+| `usr_pref_trade_{category}_amt_y{凝固年}` | 某品类该年总额 | `SUM(pay_amt_cny)` WHERE dt IN [该年 01-01, 12-31] |
+| `usr_pref_trade_{category}_cnt_y{凝固年}` | 某品类该年总次数 | `SUM(pay_order_cnt)` 同口径 |
 
-`{category}` 同 §2.4 16 品类。
+`{category}` 同 §2.4 16 品类。1 期 `{凝固年}=2025`,落 `usr_pref_trade_basketball_amt_y2025` 等 32 个 tag_code。
 
 ### 3.5 dt 锚点 + 写入
 
-- `dt='20251231'` 永久固定
-- 跑一次写入后永远不动
-- 落 `manual/backfill/{yyyymmdd}_tdm_usr_tag_y2025_o.sql` 一次性 SQL,不挂日调度
+- 按凝固年份 `dt='{yyyy}1231'`(25 年 = `dt='20251231'`,26 年 = `dt='20261231'`),永久固定
+- 每个 dt 分区跑一次写入后永远不动;新年凝固落新 dt 分区,旧分区不动
+- 落 `manual/backfill/{yyyymmdd}_tdm_usr_tag_o_y{yyyy}.sql` 一次性 SQL,不挂日调度
+- 业界 Kimball 周期快照事实表标准做法:单表 + insert-only 时间分区(粒度是周期不是事务,多年数据 `WHERE dt IN (...)` 切片即可)
 
 ### 3.6 行数估算
 
-30 万用户 × ~5 活跃品类 × 2 度量 ≈ 300 万行(永久),ORC 压缩后 < 100 MB
+30 万用户 × ~5 活跃品类 × 2 度量 ≈ 300 万行 / 凝固年,ORC 压缩后 < 100 MB / 分区。N 年累积 N × 300 万行单表 OK
 
 ---
 
@@ -167,14 +168,14 @@
 
 ## 5. 跨年扩张规则
 
-每年 1-1 凝固上一年累计为新表:
+每年 1-1 凝固上一年累计为 `tdm_usr_tag_o`  dt 分区(**不新建**)
 
 | 时点 | 动作 |
 |---|---|
-| 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`;同理 |
+| 2027-01-01 | `tdm_usr_tag_o` 新落 `dt='20261231'` 分区(凝固 26 年累计);`tdm_usr_tag_d` 移除 y2026 偏好 tag_code + 新增 y2027 tag_code |
+| 2028-01-01 | `tdm_usr_tag_o` 新落 `dt='20271231'` 分区;同理 |
 
-`_o` 表数据永久保留。下游想看历年总额按 `dt='{凝固年}1231'` 查对应 `_o` 表。
+`_o` 表数据永久保留。下游想看历年总额按 `WHERE dt IN ('20251231', '20261231', ...)` 或 `dt='{凝固年}1231'` 查 `tdm_usr_tag_o` 一张表。
 
 ---
 

+ 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 §3 + §5 改写:tdm_usr_tag_o 单表多 dt 分区代替每年一张表**:按 Kimball 周期快照事实表标准(单表 + insert-only 时间分区)+ 阿里 OneData 周期快照事实表范式 + EAV schema 永久稳定 + 1 期数据量级(300 万行 / 凝固年)远低于 sharding 阈值,原方案 A(每年一张 `tdm_usr_tag_y{yyyy}_o`)改方案 B(单表 `tdm_usr_tag_o`,按凝固年 `dt='{yyyy}1231'` 分区)。§3 标题 / 表名 / dt 锚点 / SQL 文件命名(`{date}_tdm_usr_tag_o_y{yyyy}.sql`)改写;§5 跨年扩张规则改为"落新 dt 分区"而非"新建表"。下游查询从 UNION 多表改为 `WHERE dt IN (...)` 单表多分区。README 索引描述层无变化。 | — |
 | 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(实际很难保证)。 | — |