Ver Fonte

docs(kb/29): 加 DWS 建模 + dws_usr_user_trade_1d 字段表(pay 单源 + N=2 回算)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tianyu.chu há 3 dias atrás
pai
commit
1fa483969b
3 ficheiros alterados com 114 adições e 0 exclusões
  1. 1 0
      README.md
  2. 112 0
      kb/29-dws建模.md
  3. 1 0
      kb/92-重构进度.md

+ 1 - 0
README.md

@@ -91,6 +91,7 @@ PG/ES ──DataX(raw)──> RAW ──> ODS ──> DWD ──> DWS ──> TD
 | [26-时间语义](kb/26-时间语义.md) | **时间变量约定**:T 任务日锚点 + DS 变量底层 + cdt/dt/tdt/pdt 项目参数 + raw/ods 各层 dt 语义 + 重跑幂等条件 |
 | [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 字段表 |
 
 ### 3x 开发流程
 

+ 112 - 0
kb/29-dws建模.md

@@ -0,0 +1,112 @@
+# DWS 建模
+
+> 本数仓 DWS 层(汇总层)的字段建模与设计约定。建模方法论(主题宽表 / 维度退化原则 / 业界 OneData 范式)见 `20-数仓分层与建模.md` §5;命名规则见 `21-命名规范.md` §3.5;时间语义见 `26-时间语义.md`。
+>
+> 本文档按"主题宽表一节"组织,每节包含主题定义 / 粒度 / 来源 / dt 锚点 / 度量字段说明 / 字段表。
+
+## 1. 通用约定
+
+### 1.1 框架字段
+
+所有 DWS 表必带 `etl_time TIMESTAMP` + 分区 `dt STRING`,`STORED AS ORC`。
+
+### 1.2 单一职责:日聚合
+
+DWS 只做 `_1d` 日聚合主题宽表,不爆窗口表(不建 `_30d` / `_y{年份}` 等多窗口物化)。跨周期窗口(30d / yYYYY 累计 / yYYYY 总额)由上层 TDM 跑批从 `dws_1d` 滚动聚合,TDM 按更新周期分 `_d`(日更)/ `_o`(一次性凝固)。
+
+理由:
+
+- DWS 职责单一保持稳定,避免跨周期物化的实体表爆炸
+- 跨周期物化由 TDM 表的更新周期分类(d/o)天然解决
+- 1 期数据量级 dws_1d 单年扫描 Spark 可接受
+
+### 1.3 维度退化策略
+
+业界主流(阿里 OneData)DWS 加强维度退化 + 宽表化以提升查询效率。本项目 1 期 scope 服务标签计算(仅用 `user_id × category × dt`),暂不冗余退化字段。后续触发条件评估扩展:
+
+- 上层 BI 直接查 DWS 做透视分析(按非主键维度聚合,如 cert_city / merchant)
+- 标签维度从 (user, category) 扩展到多维组合
+- 跨域 join 频繁,DWS ← DIM 重复成本不可接受
+
+补法:
+
+- 简单 = `ALTER TABLE ADD COLUMN` + 重刷历史分区(DWS 聚合层重刷代价小)
+- 复杂 = 新建第二张主题宽表(业界 1-3 张 / 主题,如 `dws_usr_user_demo_1d` 走人口学维度)
+
+### 1.4 分区与写入
+
+- 分区锚点:业务时间(与上游 DWD 对齐,如 `DATE(payment_success_time)`)
+- 写入策略:**回算近 2 日**(`dt IN (${dt}, ${pdt})`),与 DWD N=2 对齐(DWD 漂移连锁补偿,参 `93-架构决策.md` ADR-09)
+- 分区类型:动态分区 `PARTITION (dt)`
+- 重跑幂等:`INSERT OVERWRITE PARTITION (dt)` 项目默认 DYNAMIC mode(kb/26 §8)只覆盖 SELECT 出现的 dt,不动其他历史分区
+- 调度依赖:DS DEPENDENT 同 dt DWD 跑完
+
+---
+
+## 2. dws_usr_user_trade_1d(用户 × 品类 × 日 交易主题宽表)
+
+### 2.1 主题
+
+用户 × 品类粒度的日交易聚合,承载偏好标签(金额 / 次数 / 多窗口)的聚合基础。
+
+### 2.2 粒度
+
+`(user_id, category, dt)` 在分区内唯一。`category` 取 `dim_trd_card_group_ful_d.category`(DIM 已清洗,叶子品类,权威源)。
+
+### 2.3 来源
+
+`dwd_trd_order_pay_apd_d` 单源(A3 锁定 1 期不做 refund,2 期接退款时再补 refund / net 度量)。
+
+DWD 已完成维度退化(`category` 来自 `dim_trd_card_group_ful_d`,DIM 已归一脏数据),DWS 直引 DWD 的 `category` 字段,不再二次 join DIM。
+
+### 2.4 dt 锚点
+
+承袭 DWD:`DATE(payment_success_time)` 业务时间分区。
+
+### 2.5 度量字段说明
+
+按业界 DWS 宽表化原则保留多度量混存,下游 BI / TDM 不再写减法、不再 join 明细。
+
+**口径要点**:
+
+- `pay_amt_cny`(Net Revenue)= **偏好标签金额口径**。DWD 已内置 `mer_act%` 修正规则(mer_act% 时取 `point/100`,否则取 `actual_payment`),详见 kb/27 §2.5
+- `payable_amt_cny`(GMV)= **业务通用 GMV 口径**,与偏好标签解耦(业务侧 SQL 常用)
+- `pay_order_cnt = COUNT(DISTINCT order_id)` = **偏好标签次数口径**(A2 锁定,不用份数)
+- `purchase_cnt`(份数)保留作 2 期备用 + 审计,1 期标签不用
+
+**砍掉的字段**(违反"字段要全"原则但 1 期 scope 不扩张优先):
+
+- `card_price_cny` / `act_price_cny`(单价 SUM 无业务意义)
+- `discount_amount_amt_cny`(DWD 字段名未拍板,待业务侧澄清与 `merchant_discount` 语义区别)
+- `shipping_free_amt_cny`(运费券 SUM 业务意义不明)
+- `discount_point` / `give_cnt`(非偏好主流度量)
+
+### 2.6 字段表
+
+| 分组 | 字段 | 类型 | 来源 / 算法 | 说明 |
+|---|---|---|---|---|
+| 主键 | user_id | BIGINT | dwd | 用户 id |
+| 主键 | category | STRING | dwd | 叶子品类(DIM 已清洗)|
+| 度量-数量 | pay_order_cnt | BIGINT | `COUNT(DISTINCT order_id)` | 当日支付订单数(偏好次数口径)|
+| 度量-数量 | purchase_cnt | BIGINT | `SUM(purchase_cnt)` | 当日支付份数(备用)|
+| 度量-金额 | payable_amt_cny | DECIMAL(20,4) | `SUM(payable_amt_cny)` | GMV |
+| 度量-金额 | pay_amt_cny | DECIMAL(20,4) | `SUM(pay_amt_cny)` | Net Revenue(偏好金额口径)|
+| 度量-金额 | trade_amt_cny | DECIMAL(20,4) | `SUM(trade_amt_cny)` | 订单交易金额 |
+| 度量-金额 | settle_amt_cny | DECIMAL(20,4) | `SUM(settle_amt_cny)` | 结算金额 |
+| 度量-金额 | merchant_discount_amt_cny | DECIMAL(20,4) | `SUM(merchant_discount_amt_cny)` | 商家折扣 |
+| 度量-金额 | platform_discount_amt_cny | DECIMAL(20,4) | `SUM(platform_discount_amt_cny)` | 平台券折扣 |
+| 度量-金额 | member_discount_amt_cny | DECIMAL(20,4) | `SUM(member_discount_amt_cny)` | 会员折扣 |
+| 度量-金额 | act_discount_amt_cny | DECIMAL(20,4) | `SUM(act_discount_amt_cny)` | 活动折扣 |
+| 度量-金额 | point_deduct_amt_cny | DECIMAL(20,4) | `SUM(point_deduct_amt_cny)` | 积分抵扣金额 |
+| 度量-金额 | shipping_amt_cny | DECIMAL(20,4) | `SUM(shipping_amt_cny)` | 运费 |
+| 度量-积分 | point | BIGINT | `SUM(point)` | 当日消耗积分 |
+| 框架 | etl_time | TIMESTAMP | 派生 | ETL 处理时间 |
+| 分区 | dt | STRING | `DATE(payment_success_time)` | **业务时间分区**|
+
+### 2.7 不带的字段说明
+
+- **维度退化字段**(用户域 sex_cert / cert_city / level,商家域 merchant_id / merchant_name 等):1 期 scope 服务标签计算暂不冗余,触发条件见 §1.3
+- **lv1 大类 `main_category`**(业务库原名 `first_sport`):业务侧 2026-05-08 拍板 1 期不引,2 期单独处理大类聚合时再补
+- **refund / net 度量**:A3 锁定 1 期不做退款,2 期接 refund 时按业务诉求选择:
+  - 业界主流(OneData / 字节):refund 度量聚到本表 + 加 `net_amt_cny` / `net_order_cnt` 冗余列
+  - 替代方案:单独新建 `dws_usr_user_refund_1d` + 上层 join

+ 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-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 末尾"与仓库改名的联动"段压缩为一行记录 | — |
 | 2026-04-21 | **聚簇 A.4 Spark 参数外配 + `spark_sql.py` 三级覆盖**:按业务调整频率拆两文件入库 —— `conf/spark-defaults.conf`(12 条底层行为/开关类,初始化后少改:`spark.sql.adaptive/broadcastTimeout/codegen/arrow*/files/statistics.*` + `spark.dynamicAllocation.enabled` + `spark.files.ignoreCorruptFiles` + `spark.debug.maxToStringFields` + `spark.port.maxRetries` + `hive.exec.orc.default.block.size`)+ `conf/spark-tuning.conf`(10 条资源/并行度/队列,业务早期常改:`spark.{driver,executor}.{memory,cores}` + `spark.executor.instances` + `spark.executor.memoryOverhead` + `spark.driver.maxResultSize` + `spark.default.parallelism` + `spark.sql.shuffle.partitions` + `spark.yarn.queue`)。`dw_base/spark/spark_sql.py` 改造:(a) 模块级新增 `_load_spark_conf_file(path)`,读 Spark 原生 `key value` 格式,支持 `#` 注释与空行,文件缺失返回 `{}` 容错单测;(b) `__init__` 10 个 tuning 相关构造参数默认值 `'2g' / 200 / ...` → `Optional[...] = None` sentinel,不破坏既有调用点显式传参;(c) `__init_spark_session` 原 22 条硬编码 `.config(...)` 链替换为三段:L1 先 `spark-defaults.conf` 后 `spark-tuning.conf`(相同 key tuning 覆盖 defaults)→ L2 `self._final_spark_config`(SQL 内 SET)→ L3 构造参数非 None 项 + `extra_spark_config`(L3 内 extra 覆盖 named),保持原"extra > SQL SET > named" 的向后兼容;日志分层打 `L1/L2/L3` 前缀便于排查。联动:`kb/90 §2.2` conf 结构加 `spark-tuning.conf` + `§2.3` 改写为两文件模型(去单文件草案)+ 删"坑 2"(B1 → A2 依赖边)+ 聚簇 L59 依赖边删 | — |