init: 项目初始提交 - NeoZQYY Monorepo 完整代码
This commit is contained in:
554
apps/etl/pipelines/feiqiu/docs/etl_tasks/dwd_tasks.md
Normal file
554
apps/etl/pipelines/feiqiu/docs/etl_tasks/dwd_tasks.md
Normal file
@@ -0,0 +1,554 @@
|
||||
# DWD 层任务详解
|
||||
|
||||
> 本文档说明飞球 ETL 系统中 DWD(明细数据层)的所有任务。
|
||||
> DWD 层负责从 ODS 读取原始数据,经清洗、类型转换和列映射后写入维度表(dim_*)和事实表(dwd_* / fact_*),
|
||||
> 维度表采用 SCD2 或 Type1 Upsert 策略,事实表按时间增量装载。
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
DWD 层共有 2 个已注册任务:
|
||||
|
||||
| 任务代码 | Python 类 | 注册参数 | 说明 |
|
||||
|----------|-----------|----------|------|
|
||||
| `DWD_LOAD_FROM_ODS` | `DwdLoadTask` | `layer="DWD"` | 核心装载任务:遍历 TABLE_MAP,维度走 SCD2/Type1,事实走增量 |
|
||||
| `DWD_QUALITY_CHECK` | `DwdQualityTask` | `layer="DWD"`, `task_type="verification"` | ODS 与 DWD 行数/金额核对 |
|
||||
|
||||
> 注册位置:`orchestration/task_registry.py`
|
||||
>
|
||||
> **历史说明**:早期版本曾有 3 个独立 DWD 任务(TICKET_DWD、PAYMENTS_DWD、MEMBERS_DWD),
|
||||
> 通过专用 Loader 写入不存在的 `billiards.*` schema。这些任务已于 2026-02-14 废弃删除,
|
||||
> 其功能由 `DWD_LOAD_FROM_ODS` 的 TABLE_MAP 映射完全覆盖。
|
||||
|
||||
---
|
||||
|
||||
## DWD_LOAD_FROM_ODS — 核心装载任务
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `DWD_LOAD_FROM_ODS` |
|
||||
| Python 类 | `tasks.dwd.dwd_load_task.DwdLoadTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 数据来源 | `billiards_ods.*`(ODS 层各表) |
|
||||
| 数据目标 | `billiards_dwd.*`(维度表 + 事实表,共 40+ 对映射) |
|
||||
| 事务模式 | 每张表一次独立事务,单表失败回滚后继续后续表 |
|
||||
| 配置项 | `dwd.only_tables`(可选,限定只处理指定表)、`dwd.fact_upsert`(默认 `True`) |
|
||||
|
||||
### 执行流程
|
||||
|
||||
```
|
||||
extract(context) → 返回 {"now": datetime.now()}
|
||||
↓
|
||||
load(extracted, context) → 遍历 TABLE_MAP
|
||||
↓
|
||||
对每张 DWD 表:
|
||||
├─ 获取 DWD 列信息 + ODS 列信息
|
||||
├─ 判断表名前缀:
|
||||
│ ├─ dim_* → _merge_dim()(维度合并)
|
||||
│ └─ 其他 → _merge_fact_increment()(事实增量)
|
||||
├─ commit(成功)或 rollback(失败)
|
||||
└─ 记录 summary / errors
|
||||
```
|
||||
|
||||
### TABLE_MAP 映射表
|
||||
|
||||
`TABLE_MAP` 定义了 DWD 表到 ODS 表的完整映射关系。每对映射中,DWD 表名为 key,ODS 表名为 value。
|
||||
主表与扩展表(`_ex`)共享同一 ODS 源表,通过 `FACT_MAPPINGS` 中的列映射区分写入哪些字段。
|
||||
|
||||
#### 维度表映射
|
||||
|
||||
| DWD 表 | ODS 源表 | 说明 |
|
||||
|--------|----------|------|
|
||||
| `billiards_dwd.dim_site` | `billiards_ods.table_fee_transactions` | 门店维度(从台费流水的 siteprofile 快照提取) |
|
||||
| `billiards_dwd.dim_site_ex` | `billiards_ods.table_fee_transactions` | 门店扩展(灯控、WiFi、客服等) |
|
||||
| `billiards_dwd.dim_table` | `billiards_ods.site_tables_master` | 台桌维度 |
|
||||
| `billiards_dwd.dim_table_ex` | `billiards_ods.site_tables_master` | 台桌扩展(台布使用时间等) |
|
||||
| `billiards_dwd.dim_assistant` | `billiards_ods.assistant_accounts_master` | 助教维度 |
|
||||
| `billiards_dwd.dim_assistant_ex` | `billiards_ods.assistant_accounts_master` | 助教扩展(简介、分组、灯控设备等) |
|
||||
| `billiards_dwd.dim_member` | `billiards_ods.member_profiles` | 会员维度 |
|
||||
| `billiards_dwd.dim_member_ex` | `billiards_ods.member_profiles` | 会员扩展(注册来源、组织等) |
|
||||
| `billiards_dwd.dim_member_card_account` | `billiards_ods.member_stored_value_cards` | 会员储值卡维度 |
|
||||
| `billiards_dwd.dim_member_card_account_ex` | `billiards_ods.member_stored_value_cards` | 储值卡扩展(电费抵扣、冻结余额等) |
|
||||
| `billiards_dwd.dim_tenant_goods` | `billiards_ods.tenant_goods_master` | 租户商品维度 |
|
||||
| `billiards_dwd.dim_tenant_goods_ex` | `billiards_ods.tenant_goods_master` | 租户商品扩展(条码、备注等) |
|
||||
| `billiards_dwd.dim_store_goods` | `billiards_ods.store_goods_master` | 门店商品维度 |
|
||||
| `billiards_dwd.dim_store_goods_ex` | `billiards_ods.store_goods_master` | 门店商品扩展(库存、安全库存等) |
|
||||
| `billiards_dwd.dim_goods_category` | `billiards_ods.stock_goods_category_tree` | 商品分类维度(含子类展开) |
|
||||
| `billiards_dwd.dim_groupbuy_package` | `billiards_ods.group_buy_packages` | 团购套餐维度 |
|
||||
| `billiards_dwd.dim_groupbuy_package_ex` | `billiards_ods.group_buy_packages` | 团购套餐扩展 |
|
||||
|
||||
|
||||
#### 事实表映射
|
||||
|
||||
| DWD 表 | ODS 源表 | 说明 |
|
||||
|--------|----------|------|
|
||||
| `billiards_dwd.dwd_settlement_head` | `billiards_ods.settlement_records` | 结算头(订单结算主记录) |
|
||||
| `billiards_dwd.dwd_settlement_head_ex` | `billiards_ods.settlement_records` | 结算头扩展(支付方式、撤单、促销等) |
|
||||
| `billiards_dwd.dwd_table_fee_log` | `billiards_ods.table_fee_transactions` | 台费流水 |
|
||||
| `billiards_dwd.dwd_table_fee_log_ex` | `billiards_ods.table_fee_transactions` | 台费流水扩展(销售员、消费类型等) |
|
||||
| `billiards_dwd.dwd_table_fee_adjust` | `billiards_ods.table_fee_discount_records` | 台费调整/折扣 |
|
||||
| `billiards_dwd.dwd_table_fee_adjust_ex` | `billiards_ods.table_fee_discount_records` | 台费调整扩展 |
|
||||
| `billiards_dwd.dwd_store_goods_sale` | `billiards_ods.store_goods_sales_records` | 商品销售记录 |
|
||||
| `billiards_dwd.dwd_store_goods_sale_ex` | `billiards_ods.store_goods_sales_records` | 商品销售扩展 |
|
||||
| `billiards_dwd.dwd_assistant_service_log` | `billiards_ods.assistant_service_records` | 助教服务记录 |
|
||||
| `billiards_dwd.dwd_assistant_service_log_ex` | `billiards_ods.assistant_service_records` | 助教服务扩展 |
|
||||
| `billiards_dwd.dwd_assistant_trash_event` | `billiards_ods.assistant_cancellation_records` | 助教取消/废单事件 |
|
||||
| `billiards_dwd.dwd_assistant_trash_event_ex` | `billiards_ods.assistant_cancellation_records` | 助教取消扩展 |
|
||||
| `billiards_dwd.dwd_member_balance_change` | `billiards_ods.member_balance_changes` | 会员余额变动 |
|
||||
| `billiards_dwd.dwd_member_balance_change_ex` | `billiards_ods.member_balance_changes` | 会员余额变动扩展 |
|
||||
| `billiards_dwd.dwd_groupbuy_redemption` | `billiards_ods.group_buy_redemption_records` | 团购核销记录 |
|
||||
| `billiards_dwd.dwd_groupbuy_redemption_ex` | `billiards_ods.group_buy_redemption_records` | 团购核销扩展 |
|
||||
| `billiards_dwd.dwd_platform_coupon_redemption` | `billiards_ods.platform_coupon_redemption_records` | 平台优惠券核销 |
|
||||
| `billiards_dwd.dwd_platform_coupon_redemption_ex` | `billiards_ods.platform_coupon_redemption_records` | 平台优惠券核销扩展 |
|
||||
| `billiards_dwd.dwd_recharge_order` | `billiards_ods.recharge_settlements` | 充值订单 |
|
||||
| `billiards_dwd.dwd_recharge_order_ex` | `billiards_ods.recharge_settlements` | 充值订单扩展 |
|
||||
| `billiards_dwd.dwd_payment` | `billiards_ods.payment_transactions` | 支付记录 |
|
||||
| `billiards_dwd.dwd_refund` | `billiards_ods.refund_transactions` | 退款记录 |
|
||||
| `billiards_dwd.dwd_refund_ex` | `billiards_ods.refund_transactions` | 退款扩展 |
|
||||
|
||||
> 共计 **17 对维度映射**(含 `_ex`)+ **23 对事实映射**(含 `_ex`)= **40 对**映射。
|
||||
|
||||
---
|
||||
|
||||
### 维度/事实分流逻辑
|
||||
|
||||
`load()` 方法遍历 `TABLE_MAP` 时,根据 DWD 表名前缀自动分流:
|
||||
|
||||
```python
|
||||
if self._table_base(dwd_table).startswith("dim_"):
|
||||
# 维度表 → _merge_dim()
|
||||
else:
|
||||
# 事实表 → _merge_fact_increment()
|
||||
```
|
||||
|
||||
`_merge_dim()` 内部进一步判断维度合并策略:
|
||||
|
||||
| 条件 | 策略 | 方法 |
|
||||
|------|------|------|
|
||||
| DWD 表列中包含 SCD2 列(`scd2_start_time` / `scd2_end_time` / `scd2_is_current` / `scd2_version`) | **SCD2 合并**:关闭旧版 + 插入新版 | `_merge_dim_scd2()` |
|
||||
| DWD 表列中不包含 SCD2 列 | **Type1 Upsert**:主键冲突则更新 | `_merge_dim_type1_upsert()` |
|
||||
|
||||
> SCD2 列集合定义:`SCD_COLS = {"scd2_start_time", "scd2_end_time", "scd2_is_current", "scd2_version"}`
|
||||
|
||||
---
|
||||
|
||||
### SCD2 处理流程
|
||||
|
||||
当维度表包含 SCD2 列时,执行 `_merge_dim_scd2()` 方法,完整流程如下:
|
||||
|
||||
#### 1. 最新快照选取
|
||||
|
||||
从 ODS 源表中按业务主键取最新快照,使用 `DISTINCT ON` + `fetched_at DESC` 去重:
|
||||
|
||||
```sql
|
||||
SELECT DISTINCT ON (业务主键表达式)
|
||||
<列映射表达式>
|
||||
FROM billiards_ods.<ods_table>
|
||||
WHERE "fetched_at" IS NOT NULL
|
||||
ORDER BY 业务主键表达式, "fetched_at" DESC NULLS LAST
|
||||
```
|
||||
|
||||
- **业务主键**:从 DWD 表主键中剔除 SCD2 列后得到(`_strip_scd2_keys()`)
|
||||
- **列映射**:优先使用 `FACT_MAPPINGS` 中的显式映射(支持 JSON 路径提取如 `siteprofile->>'shop_name'`、类型转换如 `::numeric`),其次按同名列直接映射
|
||||
- **特殊处理**:`dim_goods_category` 表会额外读取 `categoryboxes` 列并展开子分类行(`_expand_goods_category_rows()`)
|
||||
|
||||
#### 2. 变更检测
|
||||
|
||||
将 ODS 最新快照与 DWD 当前版本(`scd2_is_current=1`)逐列对比:
|
||||
|
||||
```python
|
||||
# 预加载 DWD 当前版本(避免逐行 SELECT)
|
||||
SELECT * FROM billiards_dwd.<dwd_table> WHERE COALESCE(scd2_is_current, 1) = 1
|
||||
|
||||
# 逐行对比(跳过 SCD2 列本身)
|
||||
for col in dwd_cols:
|
||||
if col in SCD_COLS: continue
|
||||
if not _values_equal(current[col], incoming[col]):
|
||||
return True # 有变更
|
||||
```
|
||||
|
||||
`_values_equal()` 在对比前会做类型归一化:
|
||||
- **空值归一化**:空字符串 `""` 和 `None` 视为等价
|
||||
- **日期时间归一化**:朴素时间(naive)与时区感知时间(aware)统一比较
|
||||
- **布尔值归一化**:`"true"/"false"/"1"/"0"/"yes"/"no"` 等字符串与布尔值统一比较
|
||||
- **数值归一化**:字符串形式的数字(如 `"3.14"`)与 `Decimal` / `float` 统一比较
|
||||
|
||||
#### 3. 版本关闭与新建
|
||||
|
||||
对于检测到变更的记录,分两步批量操作:
|
||||
|
||||
**步骤 A — 批量关闭旧版本**(`_close_current_dim_bulk()`):
|
||||
|
||||
```sql
|
||||
-- 单主键优化:使用 ANY 数组
|
||||
UPDATE billiards_dwd.<table>
|
||||
SET scd2_end_time = <now>, scd2_is_current = 0
|
||||
WHERE COALESCE(scd2_is_current, 1) = 1 AND "<pk>" = ANY(<ids>)
|
||||
|
||||
-- 复合主键:逐条 execute_batch
|
||||
UPDATE billiards_dwd.<table>
|
||||
SET scd2_end_time = <now>, scd2_is_current = 0
|
||||
WHERE COALESCE(scd2_is_current, 1) = 1 AND "<pk1>" = %s AND "<pk2>" = %s
|
||||
```
|
||||
|
||||
**步骤 B — 批量插入新版本**(`_insert_dim_rows_bulk()`):
|
||||
|
||||
```sql
|
||||
INSERT INTO billiards_dwd.<table> (<所有列>) VALUES %s
|
||||
```
|
||||
|
||||
新版本行的 SCD2 列填充规则:
|
||||
| SCD2 列 | 值 |
|
||||
|---------|-----|
|
||||
| `scd2_start_time` | 当前时间(`now`) |
|
||||
| `scd2_end_time` | `9999-12-31 00:00:00`(表示"当前有效") |
|
||||
| `scd2_is_current` | `1` |
|
||||
| `scd2_version` | 旧版本号 + 1(新记录为 1) |
|
||||
|
||||
#### 4. 返回统计
|
||||
|
||||
```python
|
||||
{
|
||||
"processed": <ODS 去重后总行数>,
|
||||
"inserted": <新增记录数(首次出现的业务主键)>,
|
||||
"updated": <变更记录数(关闭旧版+插入新版)>,
|
||||
"skipped": <无变更跳过数>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Type1 Upsert 处理流程
|
||||
|
||||
当维度表不包含 SCD2 列时,执行 `_merge_dim_type1_upsert()` 方法:
|
||||
|
||||
1. 从 ODS 取最新快照(同 SCD2 的 `DISTINCT ON` 逻辑)
|
||||
2. 按主键去重,跳过主键为 `NULL` 的行
|
||||
3. 使用 PostgreSQL `INSERT ... ON CONFLICT DO UPDATE` 一次性写入:
|
||||
|
||||
```sql
|
||||
INSERT INTO billiards_dwd.<table> (<列>) VALUES %s
|
||||
ON CONFLICT (<主键>) DO UPDATE SET <非主键列> = EXCLUDED.<列>
|
||||
WHERE <任一非主键列 IS DISTINCT FROM EXCLUDED 值> -- 仅在有实际变更时才更新
|
||||
RETURNING (xmax = 0) AS inserted -- 区分新增 vs 更新
|
||||
```
|
||||
|
||||
> Type1 Upsert 不保留历史版本,直接覆盖旧值。SCD2 列(如果存在于列定义中)会被填充默认值(`scd2_start_time=now`, `scd2_version=1`)但不参与冲突判断。
|
||||
|
||||
---
|
||||
|
||||
### 事实表增量装载
|
||||
|
||||
非 `dim_*` 前缀的表走 `_merge_fact_increment()` 方法,核心逻辑如下:
|
||||
|
||||
#### 1. 水位线(Watermark)机制
|
||||
|
||||
事实表统一使用 `fetched_at` 列作为增量过滤依据,水位线获取优先级:
|
||||
|
||||
| 优先级 | 条件 | 行为 |
|
||||
|--------|------|------|
|
||||
| 1 | 配置了手动时间窗口(`run.window_override.start/end`) | `WHERE fetched_at >= <start> AND fetched_at < <end>` |
|
||||
| 2 | 无手动窗口 | 自动计算水位线 `_get_fact_watermark()`,`WHERE fetched_at > <watermark>` |
|
||||
|
||||
`_get_fact_watermark()` 的计算逻辑:
|
||||
- 若 DWD 表包含 `fetched_at` 列 → `SELECT MAX(fetched_at) FROM <dwd_table>`
|
||||
- 若 DWD 表不含 `fetched_at` 但有主键 → 通过 ODS JOIN DWD 取 `MAX(ods.fetched_at)`
|
||||
- 兜底 → `"1970-01-01"`(全量装载)
|
||||
|
||||
#### 2. 列映射与类型转换
|
||||
|
||||
列映射来源(按优先级):
|
||||
1. `FACT_MAPPINGS` 中的显式映射:`(dwd_列名, ods_源表达式, 可选类型转换)`
|
||||
2. DWD 与 ODS 同名列直接映射
|
||||
3. 主键兜底:若 DWD 主键在 ODS 中不存在但 ODS 有 `id` 列,自动映射 `pk → id`
|
||||
|
||||
类型转换(`_build_fact_select_exprs()`):
|
||||
- 当 DWD 列为数值类型(`integer`/`numeric`/`bigint` 等)而 ODS 列为文本类型时,自动添加 `CAST(NULLIF(CAST("<col>" AS text), '') AS <type>)`
|
||||
|
||||
#### 3. 快照去重
|
||||
|
||||
若 ODS 表包含 `content_hash` 列(`snapshot_mode=True`),则使用 `DISTINCT ON` 按主键 + `fetched_at DESC` 取最新快照,避免重复记录。
|
||||
|
||||
#### 4. 写入策略
|
||||
|
||||
根据配置 `dwd.fact_upsert`(默认 `True`)和 `snapshot_mode` 决定冲突处理:
|
||||
|
||||
| 条件 | SQL 策略 |
|
||||
|------|----------|
|
||||
| `snapshot_mode=True` 或 `fact_upsert=True` | `ON CONFLICT (<pk>) DO UPDATE SET ... WHERE <任一列 IS DISTINCT FROM>` |
|
||||
| `fact_upsert=False` 且无 `content_hash` | `ON CONFLICT (<pk>) DO NOTHING` |
|
||||
|
||||
写入后通过 `RETURNING (xmax = 0) AS inserted` 区分新增(`xmax=0`)和更新(`xmax≠0`)。
|
||||
|
||||
#### 5. 缺失主键回补
|
||||
|
||||
对于可能出现"回补旧记录"的事实表(如 `dwd_assistant_service_log`,定义在 `FACT_MISSING_FILL_TABLES` 中),
|
||||
在主增量写入完成后,额外执行 `_insert_missing_by_pk()`:
|
||||
|
||||
```sql
|
||||
INSERT INTO <dwd_table> (<列>)
|
||||
SELECT <列> FROM <ods_table> o
|
||||
LEFT JOIN <dwd_table> d ON d.<pk> = o.<pk>
|
||||
WHERE d.<pk> IS NULL
|
||||
AND o.fetched_at > <watermark> -- 同样受水位线约束
|
||||
ON CONFLICT (<pk>) DO NOTHING
|
||||
```
|
||||
|
||||
> 此步骤确保因时序乱序导致的遗漏记录能被补齐。
|
||||
|
||||
#### 6. FACT_MAPPINGS 列映射详解
|
||||
|
||||
`FACT_MAPPINGS` 是一个字典,key 为 DWD 表全名,value 为三元组列表 `(dwd_列名, ods_源表达式, 类型转换)`。
|
||||
|
||||
映射类型示例:
|
||||
|
||||
| 映射类型 | 示例 | 说明 |
|
||||
|----------|------|------|
|
||||
| 简单重命名 | `("table_id", "id", None)` | ODS `id` → DWD `table_id` |
|
||||
| JSON 路径提取 | `("shop_name", "siteprofile->>'shop_name'", None)` | 从 JSONB 字段提取 |
|
||||
| 类型转换 | `("longitude", "siteprofile->>'longitude'", "numeric")` | 提取后转 `numeric` |
|
||||
| 布尔转换 | `("is_first_limit", "is_first_limit", "boolean")` | 转布尔类型 |
|
||||
| 日期转换 | `("pay_date", "pay_time", "date")` | 时间戳截断为日期 |
|
||||
| SQL 表达式 | `("category_level", "CASE WHEN pid = 0 THEN 1 ELSE 2 END", None)` | 计算列 |
|
||||
|
||||
> 维度表和事实表共用 `FACT_MAPPINGS`(名称虽含 "FACT" 但实际覆盖所有表)。
|
||||
|
||||
---
|
||||
|
||||
### 配置与环境变量
|
||||
|
||||
| 配置项 | 来源 | 说明 |
|
||||
|--------|------|------|
|
||||
| `dwd.only_tables` | AppConfig / 环境变量 `DWD_ONLY_TABLES` | 逗号分隔的表名列表,限定只处理指定表(支持全名或短名) |
|
||||
| `dwd.fact_upsert` | AppConfig | 事实表是否使用 upsert(默认 `True`),设为 `False` 则用 `DO NOTHING` |
|
||||
| `run.window_override.start` / `end` | AppConfig | 手动指定时间窗口,覆盖自动水位线 |
|
||||
|
||||
---
|
||||
|
||||
### 错误处理
|
||||
|
||||
- 每张表独立事务:成功则 `commit`,失败则 `rollback` 并记录错误后继续下一张表
|
||||
- 主键缺失的行会被跳过并记录警告日志
|
||||
- ODS 表缺少 `fetched_at` 列时跳过该表并记录错误
|
||||
- `fetched_at` 为 `NULL` 的 ODS 行会被过滤(`WHERE fetched_at IS NOT NULL`)
|
||||
- 最终返回 `{"tables": [<各表统计>], "errors": [<失败表及原因>]}`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## DWD_QUALITY_CHECK — 数据质量检查
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `DWD_QUALITY_CHECK` |
|
||||
| Python 类 | `tasks.dwd.dwd_quality_task.DwdQualityTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 任务类型 | `verification`(校验类任务) |
|
||||
| 数据来源 | `DwdLoadTask.TABLE_MAP` 中定义的所有 ODS / DWD 表对(40 对) |
|
||||
| 输出文件 | `reports/dwd_quality_report.json` |
|
||||
|
||||
### 用途
|
||||
|
||||
在 `DWD_LOAD_FROM_ODS` 装载完成后,对 ODS 与 DWD 两端进行**行数**和**金额**的交叉核对,
|
||||
自动生成 JSON 格式的质检报表。用于发现装载过程中可能出现的数据丢失或金额偏差。
|
||||
|
||||
---
|
||||
|
||||
### 执行流程
|
||||
|
||||
```
|
||||
extract(context) → 返回 {"now": datetime.now()}
|
||||
↓
|
||||
load(extracted, context)
|
||||
↓
|
||||
遍历 DwdLoadTask.TABLE_MAP 中的每对 (dwd_table, ods_table):
|
||||
├─ _compare_counts() → 行数核对
|
||||
├─ _compare_amounts() → 金额核对
|
||||
└─ 结果追加到 report["tables"]
|
||||
↓
|
||||
写入 reports/dwd_quality_report.json
|
||||
```
|
||||
|
||||
> 注意:本任务不执行 `transform()` 阶段,直接在 `load()` 中完成查询与报表输出。
|
||||
|
||||
---
|
||||
|
||||
### 行数核对逻辑
|
||||
|
||||
`_compare_counts(cur, dwd_table, ods_table)` 分别对 DWD 表和 ODS 表执行 `COUNT(1)`,
|
||||
返回两端行数及差值:
|
||||
|
||||
```sql
|
||||
-- DWD 端
|
||||
SELECT COUNT(1) AS cnt FROM "billiards_dwd"."<dwd_table>"
|
||||
|
||||
-- ODS 端
|
||||
SELECT COUNT(1) AS cnt FROM "billiards_ods"."<ods_table>"
|
||||
```
|
||||
|
||||
返回结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"dwd": 12345,
|
||||
"ods": 12350,
|
||||
"diff": -5
|
||||
}
|
||||
```
|
||||
|
||||
- `diff = dwd - ods`:正值表示 DWD 多于 ODS(可能因 SCD2 产生多版本),负值表示 DWD 少于 ODS(可能有数据丢失)
|
||||
- 维度表因 SCD2 历史版本的存在,DWD 行数通常 ≥ ODS 行数,`diff > 0` 属于正常现象
|
||||
- 事实表理论上 DWD 行数应 ≤ ODS 行数(去重后),`diff > 0` 需要关注
|
||||
|
||||
**表名解析**:`_split_table_name()` 将全限定名(如 `billiards_dwd.dim_member`)拆分为 `(schema, table)`。
|
||||
若表名不含 `.`,则使用默认 schema(DWD 端默认 `billiards_dwd`,ODS 端默认 `billiards_ods`)。
|
||||
|
||||
---
|
||||
|
||||
### 金额列自动扫描规则
|
||||
|
||||
`_compare_amounts(cur, dwd_table, ods_table)` 自动识别两端表中的金额相关列,
|
||||
对公共列逐列汇总对比。
|
||||
|
||||
#### 扫描机制
|
||||
|
||||
通过 `_get_numeric_amount_columns(cur, schema, table)` 从 `information_schema.columns` 查询:
|
||||
|
||||
```sql
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = %s
|
||||
AND table_name = %s
|
||||
AND data_type IN ('numeric', 'double precision', 'integer', 'bigint', 'smallint', 'real', 'decimal')
|
||||
```
|
||||
|
||||
在返回的数值型列中,进一步按**列名关键词**过滤,只保留列名中包含以下任一关键词的列:
|
||||
|
||||
| 关键词 | 匹配示例 |
|
||||
|--------|----------|
|
||||
| `amount` | `pay_amount`, `discount_amount`, `actual_amount` |
|
||||
| `money` | `consume_money`, `member_money` |
|
||||
| `fee` | `table_fee`, `service_fee`, `fee_amount` |
|
||||
| `balance` | `card_balance`, `gift_balance` |
|
||||
|
||||
> 关键词定义:`AMOUNT_KEYWORDS = ("amount", "money", "fee", "balance")`
|
||||
> 匹配规则:列名(小写)包含关键词即命中,如 `pay_amount` 包含 `amount`。
|
||||
|
||||
#### 公共列取交集
|
||||
|
||||
分别获取 DWD 端和 ODS 端的金额列后,取**交集**(`set(dwd_cols) & set(ods_cols)`)并排序,
|
||||
只对两端都存在的同名金额列进行汇总对比。
|
||||
|
||||
#### 汇总对比
|
||||
|
||||
对每个公共金额列执行 `SUM()`:
|
||||
|
||||
```sql
|
||||
-- DWD 端
|
||||
SELECT COALESCE(SUM("<col>"), 0) AS val FROM "billiards_dwd"."<dwd_table>"
|
||||
|
||||
-- ODS 端
|
||||
SELECT COALESCE(SUM("<col>"), 0) AS val FROM "billiards_ods"."<ods_table>"
|
||||
```
|
||||
|
||||
返回结构(每个金额列一条记录):
|
||||
|
||||
```json
|
||||
{
|
||||
"column": "pay_amount",
|
||||
"dwd_sum": 98765.50,
|
||||
"ods_sum": 98770.00,
|
||||
"diff": -4.50
|
||||
}
|
||||
```
|
||||
|
||||
- `diff = dwd_sum - ods_sum`:非零值表示两端金额不一致,需排查原因
|
||||
- `COALESCE(..., 0)` 确保 `NULL` 值不影响汇总结果
|
||||
- 若某对表没有公共金额列,`amounts` 数组为空
|
||||
|
||||
---
|
||||
|
||||
### JSON 报表输出格式
|
||||
|
||||
报表写入路径:`reports/dwd_quality_report.json`(目录不存在时自动创建)。
|
||||
|
||||
#### 完整结构
|
||||
|
||||
```json
|
||||
{
|
||||
"generated_at": "2025-01-15T14:30:00.123456",
|
||||
"tables": [
|
||||
{
|
||||
"dwd_table": "billiards_dwd.dim_member",
|
||||
"ods_table": "billiards_ods.member_profiles",
|
||||
"count": {
|
||||
"dwd": 1200,
|
||||
"ods": 1000,
|
||||
"diff": 200
|
||||
},
|
||||
"amounts": [
|
||||
{
|
||||
"column": "balance",
|
||||
"dwd_sum": 50000.00,
|
||||
"ods_sum": 50000.00,
|
||||
"diff": 0.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dwd_table": "billiards_dwd.dwd_payment",
|
||||
"ods_table": "billiards_ods.payment_transactions",
|
||||
"count": {
|
||||
"dwd": 5000,
|
||||
"ods": 5000,
|
||||
"diff": 0
|
||||
},
|
||||
"amounts": [
|
||||
{
|
||||
"column": "pay_amount",
|
||||
"dwd_sum": 123456.78,
|
||||
"ods_sum": 123456.78,
|
||||
"diff": 0.0
|
||||
},
|
||||
{
|
||||
"column": "fee_amount",
|
||||
"dwd_sum": 1234.56,
|
||||
"ods_sum": 1234.56,
|
||||
"diff": 0.0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"note": "行数/金额核对,金额字段基于列名包含 amount/money/fee/balance 的数值列自动扫描。"
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `generated_at` | `string` | 报表生成时间(ISO 8601 格式) |
|
||||
| `tables` | `array` | 每对 DWD↔ODS 表的核对结果 |
|
||||
| `tables[].dwd_table` | `string` | DWD 表全限定名 |
|
||||
| `tables[].ods_table` | `string` | ODS 表全限定名 |
|
||||
| `tables[].count` | `object` | 行数核对结果 |
|
||||
| `tables[].count.dwd` | `integer` | DWD 端行数 |
|
||||
| `tables[].count.ods` | `integer` | ODS 端行数 |
|
||||
| `tables[].count.diff` | `integer` | 行数差值(`dwd - ods`) |
|
||||
| `tables[].amounts` | `array` | 金额列核对结果(可能为空数组) |
|
||||
| `tables[].amounts[].column` | `string` | 金额列名 |
|
||||
| `tables[].amounts[].dwd_sum` | `float` | DWD 端汇总值 |
|
||||
| `tables[].amounts[].ods_sum` | `float` | ODS 端汇总值 |
|
||||
| `tables[].amounts[].diff` | `float` | 金额差值(`dwd_sum - ods_sum`) |
|
||||
| `note` | `string` | 报表说明文字 |
|
||||
|
||||
---
|
||||
|
||||
### 注意事项
|
||||
|
||||
- **全量扫描**:本任务对 TABLE_MAP 中所有 40 对表执行全表 `COUNT` 和 `SUM`,在数据量较大时可能耗时较长
|
||||
- **不区分增量**:行数和金额对比基于全表统计,不受时间窗口限制
|
||||
- **SCD2 影响**:维度表因 SCD2 历史版本的存在,DWD 行数通常大于 ODS 行数,这是预期行为
|
||||
- **列名匹配大小写**:金额列扫描时将列名统一转为小写后匹配关键词
|
||||
- **报表覆盖**:每次运行会覆盖上一次的报表文件(`reports/dwd_quality_report.json`)
|
||||
Reference in New Issue
Block a user