# 数仓分层与建模 > 本文档记录 `poyee-data-warehouse` 数据仓库的分层架构、建模方法论与总线矩阵。 > 与命名规范(`数仓命名规范.md`)配套阅读。 ## 1. 架构原则 - **横向分层**:提高数据复用率,明确数据血缘 - **竖向分域**:指导指标体系的建设 ## 2. 分层定义 **二维视图(横向分层 × 公共维度侧柱):** ``` ┌────────────┐ ┌───────────────────────────────────────────┐ │ │ │ ADS 应用层:业务指标、面向服务端宽表 │ │ │ ├───────────────────────────────────────────┤ │ │ │ TDM 标签层:长表明细 + 宽表 + 人群包 │◄──┤ DIM │ ├───────────────────────────────────────────┤ │ │ │ DWS 汇总层:主题聚合、提供公共指标 │◄──┤ 公共维度 │ ├───────────────────────────────────────────┤ │ │ │ DWD 明细层:清洗加工 + 维度退化 │◄──┤ │ ├───────────────────────────────────────────┤ │ │ │ ODS 贴源层:类型转换、脏数据识别 │ │ │ ├───────────────────────────────────────────┤ └────────────┘ │ RAW 采集层:全字段 STRING,原样落盘 │ └───────────────────────────────────────────┘ 竖向分域(贯穿 DWD 及以上):交易 trd / 用户 usr / 商品 prd / 店铺 shp / 公共 pub ``` **分层定义表:** | 层 | 代码 | 定位 | 典型操作 | |----|------|------|---------| | 原始采集层 | `raw` | 从源系统/外部文件落地的原始数据,**全字段 STRING**,同步任务不做任何类型转换 | DataX 直接同步、CSV 解析、字段原样落盘 | | 贴源层 | `ods` | **类型转换、空值处理、脏数据识别的唯一入口**,输出类型化的干净表 | 从 raw STRING 字段做 `CAST`/`TRY_CAST`、空值兜底、去重 | | 维度层 | `dim` | 公共维度(时间、地区、渠道等),贯穿 DWD/DWS/TDM/ADS | 维度退化、缓慢变化维 | | 明细层 | `dwd` | 标准化、维度补全、异常处理;星型模型 | 宽表化、维度退化、数据质量校验 | | 汇总层 | `dws` | 建立汇总宽表,提供公共指标 | 多维聚合、主题整合 | | 主题域模型层 | `tdm` | 标签明细长表(EAV)+ 核心标签宽表(pivot)+ 人群包(远期 bitmap);按实体类型分表(usr/prd/shp),Hive 离线计算,远期可加 HBase(在线标签服务)/ ClickHouse(人群包交叉计算) | 标签计算、pivot 宽表、bitmap 圈选 | | 应用层 | `ads` | 提供数据展示、数据指标 | 面向消费端的定制聚合 | 数据流: ``` RDS PG / ES ──DataX──▶ RAW ──SparkSQL──▶ ODS ──▶ DWD ──▶ DWS ──▶ TDM ──▶ ADS │ ▼ DataX / Broker Load 服务层(Doris / CK / ES / Mongo) ``` ## 3. 主题域划分 当前划分为以下主题域,对应命名规范的 domain 代码: | 域 | 代码 | 说明 | |----|------|------| | 交易域 | `trd` | 订单、支付、退款、购物车 | | 用户域 | `usr` | 注册、登录、活跃、画像 | | 商品域 | `prd` | 商品、SKU、SPU、价格 | | 店铺域 | `shp` | 店铺、商家 | | 公共域 | `pub` | 平台、日历、地理等 | ## 4. 数仓总线矩阵 > 总线矩阵用于指导维度建模和指标体系建设,列出各业务过程与公共维度/业务维度的关系。 > 以下是模板,还未整理成正式可用的矩阵 | 域 | 业务过程 | 说明 | 时间 | 用户 | 地区 | 渠道 | 商品 | 店铺 | 活动 | 支付 | |-----------|---------------|----------------------|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:| | **交易域** | order_create | 用户提交订单(未支付) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | order_pay | 用户完成订单支付 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | order_cancel | 用户取消订单 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | refund | 用户发起退款/售后 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | cart_add | 用户加入购物车 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | **用户域** | user_register | 用户注册账号 | ✓ | | ✓ | ✓ | | | | | | | user_login | 用户登录行为 | ✓ | | ✓ | ✓ | | | | | | | user_active | 活跃行为(浏览/点击) | ✓ | | ✓ | ✓ | | | | | | **商品域** | product_expose| 商品曝光(列表/推荐位) | ✓ | ✓ | | ✓ | | ✓ | ✓ | ✓ | | | product_click | 商品点击进入详情页 | ✓ | ✓ | | ✓ | | ✓ | ✓ | ✓ | | | product_favor | 用户收藏商品 | ✓ | ✓ | | ✓ | | ✓ | ✓ | ✓ | | **店铺域** | shop_visit | 用户访问店铺 | ✓ | ✓ | ✓ | ✓ | | | | | | | shop_follow | 用户关注店铺 | ✓ | ✓ | ✓ | ✓ | | | | | ## 5. 维度建模方法论 > OLTP 业务库采用**范式建模**;OLAP 数仓特别是 DWD 层采用**维度建模**(Kimball)。 > 模型类型:**星型模型**,维度退化在 DWD 或 DIM 层完成。 ### 5.1 建模五步法 | 步骤 | 说明 | 示例(发券业务) | |------|------|----------------| | 1. 确定主题域 | 选取所属业务域 | 交易域 / 营销域 | | 2. 选择业务过程 | 从业务流程中提取可度量的动作 | 券发放 / 曝光 / 点击 / 领取 / 核销 | | 3. 声明粒度 | 明确事实表每行表示什么;优先原子粒度 | "每个用户每次领券记录";主键 = 领取 id + 幂等 id | | 4. 确认维度 | 从哪些角度切分数据 | 权益类型、发放渠道、门槛、折扣、时间、用户 | | 5. 确认事实 | 表示度量的数值字段 | 领取金额、核销金额、引导 GMV | ### 5.2 关键原则 - **同一事实表粒度必须一致**。跨业务过程合并的合并事实表,各自度量必须有同等级粒度。 - **维度退化**:将常用维度属性冗余到事实表,减少运行时 JOIN。 - **一致性维度**:公共维度全局统一(见 `dim_pub_*` 系列)。 - **原子粒度优先**:最低粒度事实可以无限上卷,高粒度事实不可下钻。 ### 5.3 建模示例 ``` 【业务过程】 券的核销 【粒度】 子订单 × 券 id (SKU 级) 【维度】 权益类型 / 领取场景 / 发放渠道 / 时间 / 用户 / 店铺 【事实】 核销金额 / 引导 GMV ``` ### 5.4 从数仓规划到物理落地的推演 左侧是抽象的规划层级,右侧是一个零售业务的具体示例: | 规划层级 | 示例(xxx 零售) | 物理落地 | |----------|----------------|---------| | 业务板块 | xxx 零售 | — | | 数据域 | 交易域 | Schema 划分 | | 业务过程 | 支付 | DWD 事实表切片 | | 度量 | 下单金额 | 事实字段 | | 维度建模 | 时间 / 区域 / 商品 | 维度表:订单表、商品表 | | 原子指标 | 下单金额 | `order_amt_cny` | | 派生指标 | 近 1 天 上海区域 支付下单金额 | ADS 汇总表字段 | | 汇总表 | `ads_trd_order_area_agg_inc_d` | 按区域聚合的下单金额日汇总 | > **关键映射**:业务过程 → DWD 事实表;度量 → 事实字段;维度 → DIM 表;派生指标 = 原子指标 + 时间周期 + 修饰词 → DWS/ADS 字段。 ### 5.5 DWD 明细粒度设计(合 vs 拆) **核心规则:粒度相同则合,粒度不同则拆。** **判断"粒度是否相同"的一个问题**:两个业务动作描述的是不是同一个实体的同一条记录? - 同一个实体 + 同一条记录(只是状态/阶段不同)→ 合 - 同一个实体 + 但每次动作产生新记录 → 拆 - 不同实体 → 拆 **典型场景:** | 场景 | 粒度关系 | 结论 | 理由 | |------|---------|------|------| | 拼团发起 vs 拼团成功 | 同一个团的状态变化 | 合 | 粒度都是团 ID,用 status + 多个时间列区分 | | 下单 vs 支付 vs 发货 vs 签收 | 同一个订单的生命周期 | 合 | 粒度都是订单 ID,每个阶段一列时间戳(对应 `21-命名规范.md` §2.2 `acc` 快照) | | 拼团 vs 订单 | 一对多 | 拆 | 团是团、单是单,粒度不同 | | 订单 vs 订单明细行 | 一对多(一个订单多个商品) | 拆 | 行项目有独立粒度 | | 浏览 vs 点击 vs 加购 | 各自独立事件流 | 拆 | 每次行为是独立记录(对应 `21-命名规范.md` §2.2 `apd` 快照) | **自检方法:** 1. **能不能自然地用一行表示?** 如果合并后一行就是一个完整业务实体(一个团、一个订单),各阶段只是多几列时间/状态字段 → 合 2. **合并后会不会出现一对多导致行膨胀?** 如果合并后同一个 ID 会出现多行 → 说明粒度不同,必须拆 3. **分析时是否总要 JOIN 回来?** 如果拆了之后大多数分析场景都要把两张表 JOIN 在一起 → 说明本来就该合 **一句话总结**:一行能完整描述一个业务实体就合,合了之后一个 ID 会出现多行就拆。 **状态时间字段的设计**:保留 3-5 个核心状态的时间字段(值为时间戳),其他非核心状态不建新列,使用原始状态码标记当前状态。后期若需要状态流水分析,新建状态流水表(`_apd_d`)而不是改主表。 ## 6. 数据同步策略 > ODS 层从业务库/埋点/爬虫数据接入。关键问题:数据是否存在物理删除,决定增量策略。 | 数据来源 | 接入方式 | 快照类型 | 备注 | |---------|---------|---------|------| | 业务库 (PG/MySQL) | DataX + CDC | `inc`(增量) | 如存在物理删除,后续推行软删除 | | 埋点 (Sensors → Kafka) | Kafka → HDFS/Hive | `apd`(追加) | 不可变事件流 | | 爬虫数据 | 爬虫落库 → DataX | `ful` 或 `inc` | 按源站特性决定 | | 维度数据 | 手工上传 / 配置化 | `ful`(全量) | 如国家映射、汇率表 | | 一次性历史 / vendor 单批交付 | 本地 CSV → `bin/csv-to-hdfs-starter.py` | `his`(一次性历史) | 永不调度,导入后入档;周期段固定 `_o` | **快照类型决策:** - 数据创建后会被修改 → `inc`(增量快照) - 数据不可变 → `apd`(追加) - 缓慢变化维、需要保留历史轨迹 → `zip`(拉链表) - 每日重刷全表 → `ful`(全量) - 一次性导入、永不再跑 → `his`(与 `ful` 严格区分:`ful` 是周期性调度的全量重刷,`his` 是导入后归档的一次性快照) ## 7. 分区与存储策略 - **分区字段**:`dt`(日期,必须);`hr`(小时,按需) - **存储格式**:ORC(列存) - **压缩**:`orc.compress=NONE`(咱不压缩,放弃一部分磁盘换 CPU/查询速度与 debug 友好度 ,后期做冷热数据时再考虑压缩) - **纠删码**:当前关闭(保持 3 副本),等 Worker 节点扩容后对冷数据启用 ### 7.1 组合快照示例 **拼团表 DWD 快照策略**:每日增量快照 + 近 7 天全量快照 + 全量拉链表 ## 8. raw 层与 ods 层的职责约定 这是本数仓的核心数据契约,所有 raw / ods 层作业都必须遵守。 ### 8.1 raw 层:schema-on-read landing - **全字段 STRING**:raw 层所有表的业务字段(不含 `dt` 分区)一律 `STRING` 类型,源库是什么类型、含不含空值、超不超长,raw 全部原样落盘 - **同步任务不做类型转换**:DataX ini 里不写 `columnType` 的类型映射(或统一填 `string`),CSV 导入时 SparkSQL 读取后也不 `CAST` - **为什么**: - 类型转换失败会中断同步链路,而 raw 是链路出入口,必须稳定 - 源库偶发脏数据(超长、空串、非预期类型)不会丢失,可追溯 - 同一份 raw 可以被 ods 重建多次(用不同清洗逻辑),而不需要回源库再同步 ### 8.2 ods 层:类型转换与脏数据识别 - **ods 是类型化的第一层**:从 raw 的 STRING 字段做 `CAST` / `TRY_CAST`,输出真正类型化的干净表 - **ods 是脏数据拦截线**:转换失败的数据不能静默丢弃,必须有可观测的出口(打标记字段、落到 `_err` 分区、或写入专门的数据质量日志表,具体策略 TBD) - **ods 不做业务语义加工**:只做"把字符串变回正确类型 + 空值兜底 + 去重",不做字段合并、维度关联、指标计算等 dwd 才做的事 ### 8.3 其他框架字段 raw 层是否需要 `etl_load_time` / `src_file` / `src_row_no` 等框架字段,暂不做统一要求,后续实际接入第一批表时再根据需要补充到本节。 ### 8.4 ods 层类型映射参考 **总则**:raw 层一律 STRING 兜底同步;类型化在 ods 层完成。下表为 ods 层 `CAST` 目标类型的参考表,具体字段可按业务需要微调(如小金额字段可下沉到 `DECIMAL(16,2)`)。 #### 8.4.1 PostgreSQL → Hive | PG 类型 | Hive 类型 | |---------|-----------| | `int2` / `smallint` | `BIGINT` | | `int4` / `integer` / `int` | `BIGINT` | | `int8` / `bigint` | `BIGINT` | | `serial` | `BIGINT` | | `bigserial` | `BIGINT` | | `numeric` / `decimal` | `DECIMAL(20,4)` | | `real` / `float4` | `DECIMAL(20,4)` | | `float8` / `double precision` | `DECIMAL(20,4)` | | `char` / `character` | `STRING` | | `varchar` / `character varying` | `STRING` | | `text` | `STRING` | | `timestamp` / `timestamp without time zone` | `STRING` | | `timestamptz` | `STRING` | | `date` | `STRING` | | `time` / `timetz` | `STRING` | | `boolean` / `bool` | `TINYINT` | | `uuid` | `STRING` | | `interval` | `STRING` | | `tsvector` | `STRING` | | `array` | `STRING`(保留 JSON/文本形态,dwd 按需解析) | | `hstore` | `MAP` | **说明**: - 整数统一 `BIGINT`:避免上游扩位(`int4` → `int8`)时下游被动改表 - 小数统一 `DECIMAL(20,4)`:覆盖绝大多数金额/比率场景;特殊精度需求(如高精度科学计算)单独评估 - 布尔用 `TINYINT`(0/1):Hive 的 `BOOLEAN` 与 ORC/Spark 生态兼容性没有 TINYINT 稳定 - 时间类型全部 `STRING`:保留源端字面量,dwd 层再按需 `to_timestamp` / `to_date` #### 8.4.2 Elasticsearch → Hive (待补,首批 ES 埋点库接入时落地) ## 9. 相关文档 - [命名规范](21-命名规范.md) — 表/字段命名五段式 - [指标体系](22-指标体系.md) — 原子/派生指标定义 - [标签体系](23-标签体系.md) — TDM 层画像/标签