93-架构决策.md 46 KB

架构决策

重构完成后沉淀的关键架构决策(ADR),给自己看的备忘。

说明

  • 本文档是重构与建模过程中关键决策、权衡、反悔条件的永久沉淀:过程文档(原重构路线 / 进度 / 对比)已删;后续演进待办见 90-演进路线.md91-重构备忘.md 为私人备忘另存。
  • 单条 ADR 格式按业界主流(Michael Nygard 版精简):背景 / 决策 / 后果 / 候选方案 / 反悔条件 五段;不套公司标准模板的多余字段。

模板

新增一条决策时,复制以下模板到 §2 列表内。编号连续,不复用已弃用编号。

### ADR-NN 决策标题

- **状态**:已采纳 / 被 ADR-MM 取代 / 已弃用
- **背景**:当时面对的问题、约束、触发决策的场景。
- **决策**:最终选了什么方案,一句话。
- **后果**:带来的好处、新增的代价、影响到的模块。
- **候选方案**:考察过但否决的方案,以及否决理由。
- **反悔条件**:什么条件下会重新评估或反悔。

决策清单

ADR-01 DataX 入口默认单日,显式 -backfill 开日循环用于存量回填

  • 状态:草案 老 spark-sql-starterget_date_range 支持 20260401-20260410 范围格式自动展开;DataX 入口从未用过。本项目调度用 DolphinScheduler,DS 原生支持业务日期补数(时间区间选定后,按调度周期逐日实例化 task)。用户老 DS 配置即 -start-date=${dt} -stop-date=${cdt} 单日传参。2026-04-24 出现场景:接入新源表需一次性回填 N 年历史存量数据——这类一次性手工批量 DS 的日历补数组件实例化太慢,走外部工具更合适。

  • 决策:DataX 入口默认单日语义start_date / stop_date 对应一个 dt 分区),T+1 增量补数仍归 DS;hive-import 入口 2026-04-24 补 -backfill flag,显式开启后 -start-date / -stop-date 作外层回填范围,按日循环调用单日逻辑,用于存量回填。

  • 后果

    • 正面:DataX 层默认职责单一(单日),T+1 增量 / 补数 / 回溯在 DS 层统一可视化;一次性存量回填通过 -backfill 内置支持,不再需要外部 bash 循环 / 临时 dispatcher
    • 负面:参数语义轻度重载(-start-date / -stop-date-backfill 模式下含义不同);DS 任务模板规范上不得加 -backfill(团队约定)
  • 候选方案

    • DataX 入口层实现"日期范围自动展开 + 多 json 分发多 worker"作为默认行为——否决(重复 DS 职责 + 增入口常态复杂度)
    • 独立 bin/datax-backfill.py 工具文件——否决(和 hive-import 共享 100% 参数,独立文件冗余;单 flag 切换更简洁)
  • 反悔条件:出现必须在 DataX 层做日期展开的默认场景(如 DS 彻底换掉);或 -backfill 被误用频繁到需物理隔离

ADR-02 分布式分发归 DolphinScheduler worker group,DataX 不重复随机

  • 状态:草案 DataX 老入口 single-job-starter.sh 内置 -random + workers.ini 权重加权随机选 worker + ssh 分发。DS 自身亦有 worker group 机制(group 绑定机器列表、task 落到 group 内一台 worker)。两层叠加:DS 选 node01 → node01 上 DataX 再 random 到 node03。【查证 kb/91 §4.4】:用户老 DS 配置不传 -random,说明 DS 层已完成分发,DataX 只在本机跑——两层分发在实际运营中就没被"同时启用"过

  • 决策:DataX 入口不做 worker 分发;分布式执行在 task 粒度靠 DS worker group 承担。DataX 入口的 select_worker 等同"返回 current_host",ssh 分支可删,workers.ini 可移除

  • 后果

    • 正面:消除两层独立随机叠加打乱 DS 负载均衡;DataX 链路大幅简化(worker.py / ssh 远端执行 / workers.ini 可裁);维护成本下降
    • 负面:单 DS 任务节点内部的批量(多 ini + -parallel)场景无法在 DataX 层散到多 worker,要分布式必须在 DS 层拆成多 task
  • 候选方案:保留 DataX 两层分发——否决,"两层独立随机"破坏 DS worker group 语义

  • 反悔条件:DS 换成无 worker group 支持的调度器;或单 task 内批量规模大到 DS 拆分成本过高

ADR-03 零点漂移决策

时间变量定义(cdt/dt/tdt/pdt 数值映射)+ 各层 dt 重跑幂等条件 详见 kb/21-时间语义

  • 状态:草案 T+1 按 update_time 过滤单日窗口 [day-start, day-stop) 同步时,源库在同步执行期间持续写入——跨零点的记录 update_time 会从 N 号"漂到" N+1 号,单日窗口无法捕获漂移记录

漂移概率和漂移数据量的相关变量:

  • 漂移窗口 = 执行时刻距零点的小时数。越晚跑漂移窗口越大
  • 漂移窗口越大,单条记录的漂移概率越高
  • 漂移窗口内用户越活跃(业务写入高峰落在漂移窗口内),漂移数据量越多

业界一般做法:小数据量、用户低活跃度的场景下,通常凌晨 0:30 前后跑 T+1,漂移窗口 30 分钟、活跃度低,单日固定窗口 [day-start, day-stop) 即可,忽略极少量漂移数据是可接受的工程权衡。

本项目特殊性:业务高峰在凌晨 6 点前,同步定时必须避开高峰定在 6 点后,漂移窗口 6 小时;且若干用户行为集中在 0-6 点,漂移窗口和活跃度两个放大因子都踩中,漂移不能忽略——需要单独设计。

极端场景佐证:某用户习惯 0-6 点更新自己的数据,若走业界做法的单日固定窗口,数仓永远只能看到该用户最早的 create_time 版本,最新状态永远抓不到。

  • 决策

    • raw 层 where 右界扩展为次日同期:where update_time >= '{day-start} 00:00' AND update_time < '{day+1-stop} 00:00',在关闭侧加 1 天 buffer 覆盖漂移
    • buffer 取 1 天而非 N 小时:定时可变更(同步执行时刻从 6 点调到 3 点或 8 点都不影响窗口语义)+ 可维护(固定区间可复跑、可回刷)
    • raw 层不纠正分区漂移:所有抓到的记录(含"漂到次日"的)按 ini dt = start_date(业务日)统一落当日分区,分区内允许含次日漂移数据
    • ods 层 Spark SQL 用动态分区 PARTITION (dt),按每行 update_time 真实日期归位
    • ods 写入模式:INSERT OVERWRITE 覆盖式 + 双源 union raw dt=T-1 + raw dt=T-2,按 WHERE DATE(update_time) = T-1 过滤,dedupe by (pk, max(update_time))。覆盖式写入支持重跑;双源 union 完整捕获 update_time ∈ [T-1, T) 范围的所有版本(含跨 raw 任务时段被业务覆盖更新跨日的早期版本)
    • 为什么双源 union 必要(具体例子):

    设定:业务日 04-27,跑批日 04-28,raw 任务凌晨 03:00 跑批,业务库覆盖式更新会推 update_time 字段。

    订单 X 的 update 时间线(业务库视角):

    • 04-27 02:00:update,update_time = 04-27 02:00(状态 A)
    • 04-28 02:00:再 update,update_time = 04-28 02:00(状态 B,跨日)

    raw 抓取过程

    • raw dt=04-26 任务(04-27 03:00 跑批,窗口 [04-26, 04-28)):业务库 X 是状态 A,抓到 update_time = 04-27 02:00 → 落 raw dt=04-26 / update_dt=04-27
    • raw dt=04-27 任务(04-28 03:00 跑批,窗口 [04-27, 04-29)):业务库 X 已变成状态 B(04-27 02:00 那条 update_time 字段被覆盖到 04-28 02:00),抓到 update_time = 04-28 02:00 → 落 raw dt=04-27 / update_dt=04-28

    ods 04-28 跑批写 dt=04-27 分区WHERE DATE(update_time)='04-27'):

    • 单源 raw dt=04-27:X update_time = 04-28 02:00,DATE=04-28,不入 → ods dt=04-27 漏 X 的 04-27 状态
    • 双源 union raw dt=04-27 + raw dt=04-26:raw dt=04-26 里 X update_time = 04-27 02:00(DATE=04-27,入),raw dt=04-27 里 X update_time = 04-28 02:00(不入)→ ods dt=04-27 完整包含 X 的 04-27 状态 A

    X 的 04-28 状态 B 第二天 ods 04-29 跑批时落 ods dt=04-28 分区,拉链表轨迹完整无断节。

    业务高峰跨零点(如本项目订单 0-6 点高峰)下,跨日漂移是常态,双源 union 是必要机制不是 defensive。

    • ods 层跨 dt 不去重:同一业务 id 允许在多个 dt 分区并存(每条代表一个"时间段状态快照")——上层拉链表(SCD Type 2)的必要基础
    • T+1 batch 容忍边界

    本设计基于 T+1 batch ETL,容忍范围明确:

    • 可救回:跨日漂移 1 次(双源 union 救回);同 pk 跨多日持续 update(每天 batch capture 当日 update_time 的最新版本)
    • 不可救回:同 pk 在两次 raw 跑批之间的"日内中间状态"——例如 10/10 02:00 状态 A → 10/10 05:00 状态 B → 10/11 02:00 状态 C,raw dt=09(10/10 03:00 跑)抓到 A,raw dt=10(10/11 03:00 跑)抓到 C,B 永久丢失。这是 T+1 batch 固有限制(只能 capture 跑批时刻 snapshot),要 capture B 必须上 CDC(见 §候选方案)

    如果没有 raw 宽窗 + ods 双源(即单日固定窗口 + 单源),会出现"幽灵更新"

    设想某用户固定在每天凌晨 2 点 update 同一条记录 X:

    • raw dt=N 任务 N+1 凌晨 3 点跑批时,X 业务库 update_time 已经变成 N+1 02:00(被覆盖到次日)
    • 单日窗口 [N, N+1) 不覆盖 update_time=N+1 02:00 → raw dt=N 漏 X
    • 第二天 raw dt=N+1 任务 N+2 凌晨 3 点跑批时,X.update_time 又变成 N+2 02:00 → 单日窗口 [N+1, N+2) 又不覆盖 → 又漏
    • 每天都漏,X 永远捕捉不到,永远漂向后方

    宽窗(48h)+ 双源 union 配套解决幽灵更新:raw dt=N 任务窗口 [N, N+2) 抓到 X update_time=N+1 02:00 漂到 N+1 范围;ods 写 dt=N+1 时双源 union(raw dt=N + raw dt=N+1)把这条早版本归位到 ods dt=N+1 分区。

  • 后果

    • 正面:不漏漂移记录;raw 简单(只管多抓、不管分对);ods 动态分区自动归位;同 id 多 dt 并存作为拉链表底层
    • 负面:raw 每日抓量约翻倍;ods SQL 必须统一动态分区写法 + 双源 union,开发规范需明文约束
  • 候选方案

    • 单日窗口 [day-start, day-stop) 固定区间(无 buffer):业界小公司通用做法,本项目因漂移窗口 + 活跃度双放大否决
    • 动态 now 右界 [day-start, now):可复现性、复跑、补数场景难处理,否决
    • 精确匹配漂移的半天 buffer [day-start, day+6h-stop):和具体同步时刻耦合,调定时就得改窗口,否决
    • ods 跨 dt 按 (pk, max(update_time)) 去重:破坏拉链表基础,否决
    • ods 单源 raw dt=T-1(不 union raw dt=T-2):业务覆盖式更新跨日漂移场景下,漏 update_time=T-1 的早期版本(业务库该记录 update_time 已被推到 T 范围,raw dt=T-1 抓到的 DATE=T 不入),拉链表轨迹断一节,否决(具体例子见 §决策"为什么双源 union 必要")
    • ods INSERT INTO 追加 + 分区内去重(早先并列方案 B):意图保留"每日 ods 跑时刻"的中间快照轨迹。但与"支持重跑"语义冲突(追加模式重跑会重复入数据),且方案 A 双源 union 已能完整捕获跨日漂移版本,方案 B 的"中间快照"价值不抵复杂度代价,否决
    • 源库 REPEATABLE READ snapshot isolation:业务库长事务风险,否决
    • CDC(PG 逻辑复制 / MySQL binlog 流读):架构正路,需独立立项,本阶段不取(见 kb/12 CDC 演进节)
  • 反悔条件:迁 CDC

ADR-04 DataX speed 三级注入(L1 conf / L2 ini / L3 CLI)

  • 状态:草案 老代码 job_config_generator.py:60-67 把 speed 三参(channel / byte / record)按工作时段硬编码:07:50-19:00 走 channel=10 / byte=10MB / record=40k(保护业务 DB),其余时段走 channel=6 / byte=256MB / record=100k。时段边界和各档值完全写死在 Python 代码里,调优需要改代码 + 发布。压测 / 回填场景下 speed 是高频调参项,需要运行时覆盖能力。

  • 决策:speed 三参走三级合并,优先级 L1 < L2 < L3。

    • L1 conf/datax-tuning.conf:项目级默认,承载宽松时段 relaxed_period.{start,stop} 边界 + strict/relaxed 两档各 3 个 speed 参值,共 8 个 key;时段配置用 HH:MM 字符串,代码解析为 HHMM 整数比较
    • L2 DataX ini 新增可选 [speed] 段:单 ini 级覆盖 channel / byte / record;一旦在 L2 显式写了,忽略时段判断直接使用
    • L3 CLI -channel / -byte / -record:本次运行级覆盖,bin/datax-{hive-import,hdfs-export}-starter.py 暴露,逐层透传到 dw_base.datax.cli gen-json 子进程,最终到 JobConfigGenerator
    • 合并发生在 JobConfigGenerator.assemble() 内存里,结果写进生成的 json(job.setting.speed + core.transport.channel.speed);ini / conf 文件本身不动
    • 时段判断采用左闭右开 [start, stop);每 key 的来源打印到 stdout 便于审计
    • schema 以宽松时段为基准(区间内 relaxed、区间外 strict):原计划 strict_period 不工作——本项目业务高峰跨午夜(12:00 至次日 06:00),HHMM 整数比较 1200 ≤ x < 0600 为空集;宽松时段在低峰期(如 06:00-12:00)天然连续不跨日
  • 后果

    • 正面:压测 / 回填 speed 调参不用改代码;时段分档逻辑可配,业务高峰期调整不用发布;单 ini 特殊场景可用 L2 覆盖;ADR 级改动只加约 350 行代码 + 10 条单测
    • 负面:参数语义复杂度上升(3 层合并规则)——通过 [tuning] log 逐层打印缓解;conf/datax-tuning.conf 是新文件需同步到所有 worker(sync-all.sh 已覆盖)
  • 候选方案

    • 只做 L1(conf 外配,不要 L2 L3)——否决,压测高频调 speed 不想改 conf 再部署
    • 参数全外配(speed + reader.fetchSize + writer.batchSize)——否决,fetchSize/batchSize 低频调,现有 ini 覆盖硬编码 够用,外配反而复杂
    • CLI 用合成 flag --datax-conf key=value,...——否决,speed 高频用独立 flag 更直观,Spark 入口风格参考
  • 反悔条件:三级注入使用场景长期 <5%(即 L2 L3 基本不用),退回单层 conf;或者 DataX 被替换

ADR-05 DWD 事实表设计:默认"事件 vs 状态"拆派组合,acc 累积快照仅留例外口子

  • 状态:已采纳

kb/20 §5.5 走"合 vs 拆"二分法(粒度相同则合粒度不同则拆),举例订单履约 / 拼团生命周期均"合到 acc 累积快照"——一行一实体,每个里程碑一列时间戳,状态单向推进。该写法属 Kimball 维度建模理论里的 Accumulating Snapshot Fact Table,在传统数仓(Teradata / Oracle / Greenplum)里是经典模式。

2026-04-26 启动埋点 + 业务库接入业务建模时重新审视,发现两个核心问题:

  1. Hive 技术栈不友好:Hive ORC 普通表不支持原生 UPDATE,acc 模式(每经过里程碑回写时间戳列)只能"INSERT OVERWRITE 整张表 / 整个分区"实现,每天扫近 N 天 ODS 重算,写入开销大、性能粗暴。要解此约束需开 Hive ACID(CDH 6 支持但少用 + 性能折损)或上 Iceberg / Hudi / Delta Lake,本项目都不具备
  2. 业务流程多有循环:拼团有审核机制(提交 → 拒绝 → 重新提交 → 再审核),订单亦有退款 / 重派单等循环。Kimball 对 acc 的前提是"每个状态只经过一次"——循环场景 acc 的时间戳列会丢失历史,不适用

期间在 inbox 收了一篇分层分区语义草稿,主张"事实表只承载事件、状态归 DIM 拉链"。讨论后认为该结论方向正确,但论证过度(直接否定 Kimball 三类事实表的全部);最终采纳折中表述:默认拆派 + 留 acc 例外口子。

  • 决策:默认采用"事件 vs 状态"拆派组合:

    • DWD 事实表只承载业务事件(不可变事实),命名 dwd_{域}_{业务过程}_apd_d,append-only,业务时间分区
    • 实体当前状态进 DIM 拉链表,命名 dim_{域}_{实体}_zip_d,SCD Type 2 每次状态变更生成新行
    • 循环状态机天然支持:事件流水任意次重复追加,DIM 拉链每次变更生成新行可完整回溯
    • acc 不一刀切禁用:极少数严格不循环、固定线性里程碑场景(如不可逆的合同审批流程)按需评估,新建 acc 表时在 PR / 设计稿里说明选型理由
  • 后果

    • 正面:
    • 与 Hive 列存"只追加 + 分区"模型天然契合,不需要 UPDATE
    • 状态查询走拉链表 is_current 切片或按 dt 切片,直观
    • 循环 / 单向状态机统一同一套建模,降低团队心智负担
    • 与阿里 OneData / 字节 / 美团 Hive 数仓主流做法对齐
    • 负面:
    • 跨表 JOIN("下单数 - 退单数 = 实际下单数")成为常态,但维度建模本就支持
    • DIM 拉链表实现复杂度高于 acc 单表(每日变更 pk 检测 + 原行 end_date 置昨天 + 新行 insert)
    • 状态查询需要 join dim 拉链而非读单表,少量查询复杂度
  • 候选方案

    • 保留老 §5.5 合派 acc:经典 Kimball 但不适合 Hive 技术栈 + 不支持循环——否决
    • 完全禁用 acc:inbox 草稿原版主张"事实表出现需要更新状态就是建模错"——过度扩大化,放弃了 Kimball 三类事实表的理论丰富性,未来引擎栈升级(Iceberg / Hudi)想用 acc 又得反悔——否决,留例外口子
    • inc 主表 + apd 流水表 fallback(kb/40 §2.2 老循环 fallback 方案):inc 只保留最新状态,状态历史靠 apd 流水还原,但状态查询要 join 流水表算最新值,远不如拉链表 is_current 直接——否决
  • 反悔条件

    • 引擎栈升级到支持高效 UPDATE 的存储(Iceberg / Hudi / Delta Lake),acc 实现成本 / 性能不再是问题,可重新评估
    • DIM 拉链表实现复杂度成为团队瓶颈(开发提效需要)
    • 实际场景中跨表 JOIN 性能 / 维护成本超出预期

ADR-06 DDL 生成器 raw + ods 双层方案(raw ✅ 2026-04-29,ods ✅ 2026-05-06)

  • 状态:已采纳

raw / ods 表 DDL 长期手写(参考 sync ini column 列表 + 类型映射)易错且重复——80+ 字段表抄一遍 PG schema 已经痛苦,未来 50 张业务表 × 2 层(raw + ods)无法维护。需要工具自动出 DDL,开发者只做表注释 / LOCATION / 字段注释微调。

  • 决策

    • 新建 bin/hive-ddl-gen.py 一脚本两层产出(独立于 datax-sync-template-gen.py,单脚本一职责)
    • 命令 python3 bin/hive-ddl-gen.py -l {raw|ods} -ini <sync.ini path> [-o [DIR]]
    • 必须显式 -l 指定层级——避免 raw / ods 误生成;不做"同时生成两层"
    • raw 层(2026-04-29 落地)
    • 输入:裁剪后 sync.ini(reader.column 已是入仓字段集)
    • 复用 datax-sync-template-gen.pyresolve_datasource + query_columns_full 拿字段中文注释 + PG 类型
    • 输出:全字段 STRING + dt STRING 分区 + ORC + LOCATION 从 ini writer.path 推
    • 头注释自己生成 placeholder(作者 / 工单 / 状态 / 配套 ini 路径),不解析 ini 头
    • ods 层(2026-05-06 落地)
    • 输入:sync.ini + 类型映射 conf(conf/pg-to-hive-type.ini
    • 类型映射定稿条款:
      • int 系(smallint / integer / bigint)统一 BIGINT:吸收 PG 字段类型升级;下游聚合无 INT 21 亿溢出风险;ORC 列存 RLE/delta encoding 实际占用看值范围不看声明类型,存储差异 < 5%
      • numeric / decimal 统一 DECIMAL(20,4):所有金额同精度避免下游 JOIN/聚合 CAST;本项目无极端高精度(>20,4)字段
      • timestamp → TIMESTAMP:Hive 原生类型,日期函数原生用;本项目集群单一时区无迁移风险(跨时区集群迁移有 ±8h 风险,迁移时再改)
      • text / varchar / character / json / jsonb → STRING;boolean → BOOLEAN;date → DATE;real / float → FLOAT;double precision → DOUBLE;bytea → BINARY
      • PG 类型 normalize:去括号参数(numeric(12,2)numeric)+ 去时区后缀(timestamp(6) without time zonetimestamp)后查表
      • 未命中规则报错让人显式补,不静默 fallback
    • 输出:typed 字段 + 末尾加 is_deleted BOOLEAN 软删归一字段(保留原 del_flg / del_time / del_flag,CASE WHEN 转换在 ods SQL 里做)+ dt STRING 分区 + ORC + EXTERNAL TABLE + LOCATION /user/hive/warehouse/ods.db/{ods 表名}
    • 不加技术字段(反悔早先草案中的 etl_time / src_sys / src_tbl 三件套):单源情况下技术字段是常量浪费存储;多源 UNION 时(如订单表 his_o + inc_d)在 SELECT 加 literal 字段即可,不必建表时固化;追溯需求由 dt 分区 + DS 实例日志 + git history 满足
    • ods 表名 = raw 表名首段 raw_ 替换为 ods_(kb/40 §3.2 一对一对应)
  • 后果

    • 正面:手写 DDL 工作量从 80+ 行降到 < 10 行微调;新表入仓周期缩短;字段中文注释从 PG pg_description 自动取,避免手抄漏
    • 负面:工具复杂度 + 单测成本;ods 类型映射 conf 维护(极端类型可能需要个例处理)
  • 候选方案

    • 维持手写 DDL:80+ 字段表手抄成本不可接受——否决
    • 合并到 datax-sync-template-gen.py--ddl raw/ods flag:单脚本多产物耦合度高,sync 模板 vs DDL 两件事语义不同——否决,独立脚本
    • DDL 同时生成 raw + ods:两层语义不同(raw 全 STRING / ods typed),合一会引入复杂分支逻辑——否决,分两次跑
  • 反悔条件

    • 业务表数稳定 < 5 张永远不再加:手写也能撑,工具失去 ROI
    • 引擎换非 Hive(Doris / ClickHouse),DDL 生成需重写
    • 跨时区集群迁移:timestamp 映射需改 STRING 避免 ±8h 偏移
    • 出现 numeric 精度 > (20,4) 的金融场景:DECIMAL 统一精度反悔回保留原精度

ADR-07 数据质量配置化方案(mask conf 双消费 + dq 注册表)

  • 状态:草案

raw / ods 入仓后需要监控两类质量问题:(a) PG 业务库 schema 变更(新增 / 删除字段)没及时通知数仓 → 字段悄悄漏入或老字段入仓后失效;(b) PG vs Hive 行数不一致 → 同步任务可能漏抓数据。当前依赖人工巡检不可持续。merchant_open 是 schema drift 实例:PG 业务库有此字段,分析师库没同步过来,本次 generator 跑出来才发现——证明流程缺失,需 dq 任务补齐。

  • 决策

一、复用 mask conf 作 schema drift 探查的 ground truth

jobs/raw/{域}/{table}.mask.ini 是单一真值——sync 生成器和 dq 模块双消费:

  • sync 生成器:读 mask conf 出几乎可用 ini(已实施,见 ADR-06 / sync 脚本 -mask-conf
  • dq 模块:读 mask conf trim 字段集 + ini reader.column 保留集 = "数仓已知字段全集",与 PG 实时字段集做差,差非空 → schema drift 告警

二、注册表机制

conf/dq.ini 列出需 dq 任务的表:

  [card_group_order_info]
  domain = trd
  sync_ini = jobs/raw/trd/raw_trd_card_group_order_info_inc_d.ini
  mask_conf = jobs/raw/trd/raw_trd_card_group_order_info_inc_d.mask.ini
  count_threshold = 0.01

dq 任务 daily 跑:遍历注册表 → 每张表跑两类检查 → 异常推 alerter。

三、检查类型

  1. Schema drift 探查

    • 拿 PG 实时字段集(pg_catalog 查询)
    • 拿数仓已知字段集(mask conf trim ∪ ini reader.column)
    • 对称差非空 → alerter 推 "新增 [...] / 已删 [...]"
    • 数仓 review → 决策(保留 / trim / 脱敏)→ 更新 mask conf + sync ini + DDL + kb/22
  2. 数据量比对

    • PG count(*) WHERE update_time ∈ [day-start, day+1-stop)
    • Hive count(*) WHERE dt='{业务日}'
    • 差额 / PG 总量 > count_threshold → alerter
    • raw 48h 宽窗机制下两边可能略偏,阈值容忍

四、实现归属

  • dw_base/dq/(kb/92 已建空骨架)
  • 子模块:schema_drift.py + count_check.py + runner.py(读 conf/dq.ini 调度)
  • 入口:bin/dq-runner.py 命令行 / DS 调度
  • 复用 datax-sync-template-gen.pyresolve_datasource + query_columns_full + pg8000

  • 后果

    • 正面:流程缺失(schema drift / 行数差)由 dq 任务补齐,不再依赖人工巡检;mask conf 单一真值消除"裁剪记录散落 ini + kb 双写"
    • 负面:dq 任务上线前需稳定 mask conf 格式(已在 ADR 锁定);告警阈值需调试期摸合理值
  • 候选方案

    • 不做 dq,靠人工巡检:表数 ≥ 10 张后撑不住——本阶段(< 5 张)允许暂缓但不否决
    • 用现成开源 dq 工具(Great Expectations / Apache Griffin 等):本项目数据量 / 表数不需要重型框架,自己写薄壳更可控——否决重型工具
    • mask conf 不复用,dq 单独维护字段清单:双写 ground truth 易漂移——否决,复用 mask conf
  • 反悔条件

    • dq 任务范围扩大到字段值校验 / 维度一致性 / 业务规则等复杂检查,自研薄壳吃不下,迁 Apache Griffin
    • mask conf 格式本身需要重大调整,dq 模块可能要解耦自己维护
  • 实施节奏:表数 ≥ 5 张时启动;< 5 张靠人工巡检 + 偶尔重跑 sync 生成器 diff。

ADR-08 DIM ful_d 跑批:业界主流模式 B + 字段 CASE WHEN 整组判断

  • 状态:已采纳

dim ful_d 全量快照需要每日产出最新分区,增量更新方式选型。Hive / Spark 2.4 不支持 MERGE INTO(Hudi / Iceberg / Delta Lake 才有);业务库存在"主动置 NULL"场景(用户解绑某属性时业务库直接写 NULL),字段级 COALESCE 在新值是 NULL、老值非 NULL 时会错误地用昨日值兜底导致 dim 数据滞后。

本项目第一张 dim 表 dim_usr_user_ful_d(kb/24 §2)落地时定下范式,未来所有 dim_*_ful_d 表沿用。

  • 决策

    • init(首次回刷):扫 ods 历史分区 dt < ${dt} + ROW_NUMBER 取每 pk 最新版本,落 dim.dt=${pdt} 单分区
    • sche(日常增量):今日 ods 增量(dt=${dt})+ 昨日 dim 基线(dt=${pdt})+ UNION ALL
    • today_changed = 今日 ods 各源表(如 base / cert)变更 pk 集合(UNION)
    • today_rebuilt = today_changed 这些 pk 重 join 形成新 dim 行(FULL OUTER JOIN 多源 + LEFT JOIN 昨日 dim 兜底)
    • unchanged = 昨日 dim 中 pk NOT IN today_changed 的行直接保留
    • INSERT OVERWRITE dim.dt=${dt} PARTITION
    • 字段合并用 CASE WHEN 按"组"判断,不是字段级 COALESCE:
    • 如 base 字段统一 CASE WHEN bu.id IS NOT NULL THEN bu.x ELSE o.x END
    • cert 字段统一 CASE WHEN ci.user_id IS NOT NULL THEN ci.x ELSE o.x END
    • 派生字段(如 is_cert)按业务语义自定义 CASE WHEN
    • init 与 sche 同日上线:sched=T 时 init 灌 ${pdt}(T-2)+ sche 写 ${dt}(T-1),链路打通无空窗
  • 后果

    • 正面:
    • 业界主流(阿里 OneData / 字节 / 美团 Hive 数仓 SCD Type 1 增量标准范式)
    • dim 大表只 scan 不 shuffle(broadcast NOT IN / LEFT ANTI 剪枝);shuffle 量小性能稳定
    • "主动置 NULL" 安全:业务库改 NULL 整组字段同步反映到 dim,不被昨日值错误兜底
    • init / sche 共用字段表 + 同日打通无空窗
    • 负面:
    • 多源(如 base + cert)合并时 CASE WHEN 字段表冗长(每字段 1 行 CASE WHEN)
    • sche 写法长但结构对称,每张多源 dim 表都要重复一遍
  • 候选方案

    • 每日全量重算(扫 ods 历史 + ROW_NUMBER 不依赖昨日 dim):实现最简但每天扫 ods 多 dt 分区文件 IO 开销大;数据量增长后性能下降——否决
    • FULL JOIN + 字段级 COALESCECOALESCE(bu.field, o.field)):写法紧凑但业务库主动置 NULL 时 COALESCE 用昨日值替代今日 NULL 导致 dim 数据滞后——否决
    • 扫 ods 历史 + broadcast id IN today_changed 剪枝(不依赖昨日 dim 拉字段):扫 ods 多 dt 分区文件 IO 开销 vs 现版只扫今日单分区——否决,扫描成本更高
    • MERGE INTO(Spark 3+ / Iceberg / Hudi / Delta Lake 原生支持):语义最清晰性能最好;本项目 Spark 2.4 + Hive ORC 不支持——本阶段不取
  • 反悔条件

    • 迁 Spark 3+ / Iceberg / Hudi / Delta Lake 后改 MERGE INTO(更优)
    • dim 表数据量增长导致 sche shuffle 性能下降,评估改"分区裁剪 + 局部重算"
    • 业务库消除"主动置 NULL" 场景后,可放宽到字段级 COALESCE 简化写法

ADR-09 DWD 事件表跑批回算窗口(N=2 → 滚动 N=30)

  • 状态:已采纳(滚动 N=30,临时方案;目标态走 ADR-12 支付/退款独立 dwd)

  • 修订历史

    • 2026-05-10:初版 N=2(commit a9b6eaa),按"跨零点 OLTP 漂移 ≤ 1 天"通用假设
    • 2026-06-03:退款窗实测后改 滚动 N=30,写清 N=2 暴露的问题与 N=30 依据
  • 背景(N=2 的三个问题)

ods 层按 ADR-03 严格按 update_time 归位 dt。dwd 事件表按业务时间(payment_success_time)分区。N=2 初衷只兜底跨零点漂移(payment_success_time=T-1 23:59update_time 延迟到 T 落 ods.dt=T),但实际暴露三个问题:

  1. 只 cover ≤1 天漂移,cover 不到订单状态机生命周期:订单支付后历经发货/收货/退款,update_time 持续刷新,退款窗长达 15~20 天(见下"实测")。N=2 窗外的退款/状态变化不被回算 → dwd 状态过期(窗外退款单 dwd 仍记有效)
  2. 调度中断 + 补数场景缺数:调度断几天后补数,某业务日的单 update_time 已漂到数天后的 ods.dt,N=2 窄窗(只扫 ods.dt IN (dt,pdt))扫不到漂走的版本 → 缺数(2026-05 实测 dwd.dt=20260522 缺 31%)
  3. (叠加 status-before-rownum bug,已单独修复:status 移到 ROW_NUMBER 外层对 id 最新版本判,见 backfill / 日调度 SQL)
  • 退款窗实测(2026-06-03,定 N=30 依据)

5 月退款单 90,769(refund_time 100% 非空),DATE(refund_time) - DATE(payment_success_time) 分布:

| 延迟 | 占比 | |---|---| | 0-7 天 | 39% | | 8-15 天 | 58% | | 16-30 天 | 2.8% | | 30+ 天 | 0 |

P50=9, P90=P95=15(硬边界), P99=20, MAX=20。退款占非有效态 98%,故退款窗 ≈ 订单状态稳定窗。业务方"15 天退款窗"= P95,准确。

  • 决策:DWD 事件表日调度改 每天滚动重算最近 N=30 天

    • 每天 start_date = today-30stop_date = cdt(>退款窗 MAX 20 + buffer,覆盖所有退款/状态变化)
    • 复用 ADR-11 backfill 机制(宽扫窄落 + status 在 ROW_NUMBER 外层判最新版本):ods.dt 宽扫 [start_date-1, 不限]、payment 窄落 [start_date, stop_date)
    • 滚动窗内每天全覆盖重算 → dwd 恒 ≈ PG 当前快照;>30 天的老数据退款窗已过、稳定,不重算
    • 成本:~30 天 ≈ 200万单/天,几分钟(比每天全量 2 小时轻、比 N=2 准、比"当月重算"均匀不受月边界影响)
  • 为什么不是 N=2 也不是全量

    • N=2 < 退款窗 20,窗外漂移漏(上述问题 1/2)
    • 每天全量(实测 2 小时)太重,不可每天跑
    • 滚动 N=30 是"覆盖退款窗 + 成本可控"的平衡点
  • 临时性 + dwd 历史可变可接受:N=30 滚动是"快照口径"——每天重算最近 30 天,dwd 历史分区在滚动窗内随状态变化(如退款)而变,不满足"历史不可变"。但这可接受:dwd 是 ods 的可重建派生,不是历史的唯一存储;ods 跨 dt 保留同 id 多版本 = 历史真源,dwd 任何时点都能从 ods 回放重建(本次全量 backfill 从 ods 重建整个 dwd 2 小时跑通,已实证)。即"不可变性"靠上游 ods 保证、不靠 dwd —— dwd 历史怎么变都不丢数据。目标态见 ADR-12:支付事件(不筛 status,收全部 payment_success_time IS NOT NULL)+ 退款独立表,净值 = 支付 LEFT JOIN 退款,届时支付事件不可变、无需回算窗。演进时同样从 ods 回放拆建(路径已由本次全量 backfill 验证)。

  • 后果

    • 正面:覆盖退款窗全程,dwd ≈ PG;调度中断 ≤30 天可自愈(滚动窗自然 cover);统一 backfill 机制(日调度 = 每天滚动 backfill)
    • 负面:每天重算 30 天(30× 单日成本,但绝对值几分钟可接受);数据量大涨后需评估降 N 或迁 ADR-12
  • 候选方案

    • N=2:窗外退款漏、补数缺数——否决(本 ADR 起因)
    • 每天全量:实测 2 小时,太重——否决
    • 每天当月重算(month-to-date):月末跨月退款漏(5-28 单退款到 6 月),覆盖不均——否决,滚动 30 天更均匀
    • MERGE INTO(Spark 3+ / Iceberg / Hudi):引擎原生 upsert 无需回算——Spark 2.4 不支持,本阶段不取
    • 支付/退款独立 dwd(ADR-12):目标态,本阶段先滚动 N=30 过渡
  • 反悔条件

    • 数据量大涨,滚动 30 天成本不可接受 → 降 N 或迁 ADR-12
    • 退款窗实测变化(业务流程调整)→ 重测调 N
    • 迁 Spark 3+ / Iceberg / Hudi → MERGE INTO
    • ADR-12 支付/退款独立落地 → 本 ADR 回算窗下线

ADR-10 TDM 跨层下钻 DWD(1 期专用,dws 单下游场景)

  • 状态:已采纳

1 期 TDM 用户标签的消费明细统计需按拼团订单(order_type='group')× 消费品类粒度聚合。上游 dws_usr_user_trade_1d 是通用日聚合宽表(粒度 user × category × dt,按 OneData 范式不内嵌业务过滤 order_type),聚合粒度内已混合所有 order_type,TDM 层无法 WHERE 反向过滤(信息已聚合丢失)。

业界主流三种解法:

  • A. 多维粒度宽表(OneData / 阿里主流):dws 加 order_type 进粒度 = (user, category, order_type, dt),下游各自 WHERE
  • B. 分粒度事实(Kimball-ish):每业务过程一张 dws 表(dws_usr_user_grouptrade_1d 等)
  • C. 应用层跨层下钻(OneData 不推但实务常见):dws 不动,应用层从 dwd 二次聚合

1 期 dws 上层无 BI / ADS 消费方,tdm 是 dws 唯一下游,A/B 的"通用宽表 / 分粒度"价值不成立。

  • 决策:1 期 tdm_usr_tag_d / tdm_usr_tag_o 的 stat 段跨层取数 dwd_trd_order_pay_apd_d

    • 直接从 dwd 订单粒度按 (user_id, category) 重新 GROUP BY
    • WHERE 显式加 order_type='group' + category IS NOT NULL
    • 金额 SUM(pay_amt_cny);次数 COUNT(DISTINCT order_id)(dwd 订单粒度,需 DISTINCT 而非 SUM)
    • dws_usr_user_trade_1d 不动,保通用日聚合语义,留作未来 BI / 2 期接入
  • 后果

    • 正面:
    • dws 通用聚合语义不污染(不写死 order_type='group'),未来多消费方接入零反悔成本
    • tdm SQL 显式 WHERE 业务过滤,意图清晰
    • 单下游场景下不为虚构的"通用价值"付多维粒度的复杂度(kb/25 §1.2 1 期数据量级 Spark 单年扫描可接受)
    • 负面:
    • 重复聚合(dws 已 GROUP BY 一次,tdm 又 GROUP BY 一次)Spark IO 倍增;1 期数据量级可接受
    • 长期看是 1 期权宜,演进到下游多消费方时反悔到 A 多维粒度需 dws 重跑全量 + tdm SQL 改回拉 dws
  • 候选方案

    • A 多维粒度宽表(dws 加 order_type 维度):OneData 主流;1 期单下游,dws 行数翻几倍 + dws 重跑全量;Spark IO 反而比跨层下钻多——否决但为反悔目标
    • B 分粒度事实(建 dws_usr_user_grouptrade_1d 等多张专表):Kimball-ish;1 期单业务过程无表数爆炸合理性——否决
    • dws WHERE order_type='group' 写死(早期草案):dws 名为通用却按业务过滤,语义错位 + 未来 BI 想看全量订单时反悔——否决
    • 保持 tdm 从 dws 拉但混所有 order_type(不加过滤):1 期业务规约是拼团粒度,混合 order_type 数据语义错——否决
  • 反悔条件

    • BI / ADS 接入 dws 需多 order_type 切片 → 演进到 A 多维粒度宽表,tdm 改回从 dws 拉
    • dwd 30 天 / 全年扫描 Spark 性能不可接受(1 期数据量级判定失效)
    • 2 期单买 / 限时 / 活动等其他 order_type 同样需要标签 → 演进到 A 多维粒度
    • 多下游接入 dws 形成"重复聚合 × N"的代价上升 → 演进到 A

ADR-11 DWD 事件表补数任务流(backfill 与 incremental 分离)

  • 状态:已采纳

  • 背景:规划 dwd 补数(backfill,调度中断后回刷历史区间)能力。

  • 决策:dwd 补数与日调度(ADR-09)职能分离为两个独立 DS workflow,但共用同一参数化 SQL(2026-06-03 合一:原独立 _backfill.sql 与日调度逻辑 100% 重合,仅 start_date 来源不同,合并到 jobs/dwd/trd/dwd_trd_order_pay_apd_d.sql 避免两份漂移)。核心是 宽扫窄落 —— ods.dt 宽扫(捞回漂移到后续分区的版本)、payment 业务时间窄落(锁定覆盖哪些 dwd 分区):

    • 共用 SQL jobs/dwd/trd/dwd_trd_order_pay_apd_d.sql(参数化 start_date/stop_date)
    • 补数 workflow:手动触发,不挂 schedule,不加 DEPENDENT(日调度 workflow 见 ADR-09,挂 schedule 传 start_date=$[yyyyMMdd-30])
    • 参数(对齐 DataX start_date/stop_date 左闭右开范围惯例,下划线):
    • ${start_date}:workflow 启动参数(补数起始日)
    • ${stop_date}:取 ${cdt}(DS globalParam=today),左闭右开 < stop_date 即覆盖到 today-1(当天不落交日调度)
    • rawScript:spark-sql-starter.py -f <backfill.sql> -p start_date=${start_date} -p stop_date=${cdt}(不传 -dt,SQL 不用 ${dt}
    • payment 业务时间窄落 [${start_date}, ${stop_date}) 左闭右开
    • ods.dt 扫描 [${start_date}-1, 不限]
    • 下界 start_date-1:往前 1 天对齐 ADR-09 日调度 pdt buffer(cover create_time 早于 payment 落前一天的边缘)
    • 上界不限:扫到 ods 最新分区。payment ∈ 范围内的订单,其 ods 版本落点必 ≤ 最新,故全部捞到 —— 不依赖"猜漂移天数"
    • dim 退化 join 用 ${stop_date}-1(=today-1=最新 dim 快照分区)
    • 范围内每个 dwd 分区全覆盖(INSERT OVERWRITE 冲掉旧 / 缺失版本)
    • 前提:ods 已补到 today-1 + dim_trd 补到 today-1(dwd 补数从 ods 重新归位、退化 join 最新 dim)
  • 后果

    • 正面:补数完整修复任意漂移天数(不限 ods.dt 结束);与日调度窄窗互不污染;不依赖"猜 N"
    • 负面:维护两份 dwd SQL(日调度 ADR-09 + 补数本 ADR);补数扫描量大(一次性、大数据集群非业务库,可接受)
  • 候选方案

    • 扩大 ADR-09 日调度 N(如 N=5~7)统一处理日调度 + 补数:日调度每天扫 N 天成本翻倍;长中断(> N)仍救不了;漂移天数不可预知 → 否决。补数"不限 ods.dt 结束"比固定 N 稳
    • 补数按天循环跑日调度 SQL:中断长时 update_time 漂走,窄窗拉不到漂移版本 → 否决
    • 跳过 raw/ods 直接从 PG 灌 dwd:绕过链路 + 技术债 → 否决
    • CDC(Debezium / Flink CDC):保留完整变更日志根治"PG 不保留历史" → 重组件本阶段不上
  • 反悔条件

    • 上 CDC → 补数任务流可下线
    • 迁 Spark 3+ / Iceberg / Hudi → MERGE INTO
    • dwd 事件表增多 → 补数 SQL 抽象成通用模板
  • 运维定位

    • 补数是调度中断的运维兜底;日调度保持连续(中断 ≤ N=2 天由 ADR-09 自愈),超期用本任务流回刷
    • 调度中断越久,补数 start 越往前,覆盖区间越大
    • 业务时间远早于补数 start 的极端漂移,由定期校验对账(Hive vs PG)兜底

ADR-12 dwd 订单事实表演进:支付/退款事件分离(待办)

  • 状态:草案(方向记录,暂不实施)

  • 背景:当前 dwd_trd_order_pay_apd_dstatus IN (101,103,104,105,106,301,302) 取"当前有效已支付订单"快照,是从数据分析师实时业务库口径(取当前未退款)改编而来。问题:事件按 payment_success_time 分区落盘后,订单后续退款使 status 漂出集合,但 dwd 回算窗 N=2(ADR-09)外不再刷新 → 老分区状态过期,dwd 略高估"当前有效"(对账 Hive > PG 的来源)。payment_success_time IS NOT NULL 经实测是干净的支付成功判据(待支付/取消 0% 非空、已支付及之后 99.5%+)。

  • 决策方向:支付事件 + 退款事件各 append-only,净值在下游组合。

    • dwd_trd_order_pay_apd_d:筛 payment_success_time IS NOT NULL,收全部支付成功事件,status 仅存字段不过滤
    • dwd_trd_order_refund_apd_d(新):退款事件,独立表,按退款时间分区
    • 净 GMV / 当前有效 = 支付 LEFT JOIN 退款(支付事实落盘 status 固化,无法靠 dwd_pay 的 status 切退款,故退款必须独立成表)
  • 当前权宜:继续 status 口径。2026 退款占比仅 4.31%(47.6万/1104万),status 口径真正高估的只是"N=2 窗外才退款"那部分(< 4.31%),对 tdm 标签 / 日常分析可接受。

  • 反悔触发:需要精确历史 GMV / 退款分析 / dwd 当权威口径时实施。

  • 阻塞:业务库退款字段口径混乱(refund_time / refund_success_time / refund_fee / refund_status 等多字段),退款判据需先查清才能建退款表。

  • 附口径确认(2026 PG 实测):302 订单结束(未中卡) 占 2026 的 35%(390万),算消费偏好(拼团类刮刮乐,参与即反映品类偏好,中不中卡不影响信号),dwd 含 302、tdm 统计含未中卡,确认无误。

ADR-13 埋点重构:收敛事件粒度(行为大类+参数化)+ 补齐归因绑定字段

  • 状态:草案(数仓侧主张,待与产品对齐推动;存量事件不动、增量优先)

  • 背景:现行 HS 埋点(神策 SDK)按"一个页面/按钮一个事件"设计,已上线 164 个自定义事件(xlsx 定义 197),大量是 HomepageZTCKClick / BottomZTCKClick / MyCPCKClick 这类"页面+动作"具名事件。三个问题:

    1. 事件爆炸:每加一个页面就加一个事件,且每个事件 params schema 各异;
    2. 数仓结构化层直接受害:raw 是薄表 JSON(schema-on-read,不受影响),但 ods/dwd 无法做统一埋点事实宽表——每个事件得单独映射拍平;新页面=新事件=ods/dwd 建模跟着加,永远收敛不了(2026-06 埋点建模卡顿的根因);
    3. 跨事件分析(全站点击/曝光/漏斗)困难。

神策"事件-属性模型"、Amplitude "事件=动词 / 属性=上下文" 均主张少而结构化的事件 + 丰富属性;当前设计是典型 event explosion 反模式。

2026-06-10 世界杯活动漏斗需求暴露第二类缺陷(信息缺失,非粒度):想做世界杯活动转化漏斗,发现 (a) 订单/事件无活动(campaign)绑定字段 → 无法把转化归因到具体活动;(b) 关键交互缺统一参数(page/module/element)→ 无法跨事件拼路径;(c) 业务路径含线下微信群对接(app 浏览 → 微信群与商家对接 → app 下单),中间跳到平台外不可观测。强归因/路径漏斗天然做不了,最终退到拼团通用聚合 UV 漏斗(见 workspace/20260610 漏斗 MVP)。结论:埋点除事件爆炸(粒度)外,还缺"业务绑定/归因信息"——故本 ADR 从"事件粒度"扩为"埋点重构"两条腿。微信群那跳是平台外断点,埋点重构也救不了,属业务流程边界。

  • 决策:埋点重构两条腿并行(数仓主张,推动产品/后端):

① 收敛事件粒度(行为大类 + 参数化)

  • 导航/UI 长尾交互(各种 XxxClick / XxxView / XxxShow / XxxShare)→ 收敛为通用 Click / View / Exposure / Share,页面/区块/元素/位置作标准化属性page / module / element / position / content_id 等)
  • 核心业务转化事件PayOrder / GroupPayOrder / SignUp / 兑换 / 退款)保留独立事件——属性丰富、漏斗关键、语义重

② 补齐业务绑定/归因字段

  • 关键事件/订单补 campaign_id(活动绑定)、order_source(订单来源)、统一交互参数(page/module/element/content_id)——让转化能归因到活动、平台内路径能拼
  • 范围边界:仅解决"平台内可观测"部分;跨端/线下断点(如 app→微信群→app 下单的微信群那跳)是业务流程问题,埋点重构救不了,不在 scope

共同前提

  • 不推倒重来:存量事件 + 已上线 + 历史数据不动;增量优先(新交互按大类设计 + 带绑定字段),长尾逐步归并,无硬切换

  • 后果

    • 正面:埋点事件集有界;新页面=属性新值、仓库零改动(schema 稳定);ods/dwd 可建统一埋点事实表(标准维度 + typed params);跨事件漏斗/热力天然可做;补绑定字段后活动转化归因 + 平台内路径漏斗可做(世界杯漏斗需求即受益);埋点开发 + 数仓改表 + 测试成本同步下降
    • 负面:需建立并维护参数命名规范(page/module/element/position taxonomy);需后端在下单/关键事件落 campaign_id / order_source 等绑定字段,有改造成本;产品/分析师从"具名事件"转向"属性筛选"有学习成本;过渡期新老并存;部分 BI 漏斗对离散事件更友好
  • 候选方案

    • 维持一页一事件:event explosion,仓库随页面无限改——否决(本 ADR 起因)
    • 全塞进单个 Click 事件:丢核心业务事件的丰富语义、漏斗难用——否决,核心转化事件保留独立
    • 纯全埋点(神策 $AppClick 自动采集):缺业务上下文(哪个拼团/商品)——否决,仍需自定义属性补语义(即本方案的参数化)
  • 反悔条件

    • 产品坚持不改 → 数仓退守:在 ods 用映射表把长尾事件归一到行为大类(治标不治本)
    • 引入半结构化无痛 schema 漂移的引擎(全 JSON + schema-on-read 贯穿),event explosion 的仓库代价下降
    • 团队规模 / 分析诉求变化使具名事件 ROI 反超