# SCD2 缓慢变化维处理规则

本文档定义 `dwd` 模式下维度表的 SCD2（Slowly Changing Dimension Type 2）处理策略、
生效区间管理和版本控制规则。

---

## 1. 概述

### 1.1 什么是 SCD2

SCD2 通过保留维度记录的历史版本来追踪属性变化。当被跟踪字段发生变更时：
1. 关闭当前版本（设置结束时间、标记为非当前）
2. 插入新版本（设置开始时间、标记为当前）

### 1.2 实现模块

- 处理器：`tasks/dwd/dwd_load_task.py` — `_merge_dim_scd2()` 方法
- 变更检测：`_is_row_changed()` — 比较所有非 SCD2 控制列，任一列值不同即视为变更
- 批量关闭：`_close_current_dim_bulk()` — 批量设置旧版本的 `scd2_end_time` 和 `scd2_is_current = 0`
- 批量插入：`_insert_dim_rows_bulk()` — 批量插入新版本行

### 1.3 变更检测逻辑

`_is_row_changed(current, incoming, dwd_cols)` 遍历目标表的所有列（排除 SCD2 控制列），逐列比较当前版本与新数据。比较时会进行类型归一化处理：
- 空值归一化：`None`、空字符串、`"null"` 视为等价
- 数值归一化：字符串形式的数字与 `Decimal`/`int` 比较前先转换
- 布尔归一化：`"true"`/`"1"`/`"yes"` 等与 `True` 视为等价
- 日期归一化：字符串形式的日期与 `datetime` 比较前先解析

---

## 2. SCD2 元数据字段

所有维度表统一包含以下 SCD2 控制字段：

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `scd2_start_time` | `TIMESTAMPTZ` | `now()` | 版本生效起始时间 |
| `scd2_end_time` | `TIMESTAMPTZ` | `'9999-12-31'` | 版本失效时间（`9999-12-31` 表示当前有效） |
| `scd2_is_current` | `INT` | `1` | 当前版本标记（`1` = 当前，`0` = 历史） |
| `scd2_version` | `INT` | `1` | 版本号（自增） |

### 约束

- 主键：`(natural_key, scd2_start_time)` — 同一自然键的不同版本通过生效时间区分
- 唯一索引：`WHERE scd2_is_current = 1` — 保证每个自然键只有一条当前记录

---

## 3. 处理流程

```
_merge_dim_scd2(cur, dwd_table, ods_table, dwd_cols, ods_cols, now)
    │
    ├── 1. 从 ODS 取最新有效版本（DISTINCT ON + is_delete IS DISTINCT FROM 1）
    │
    ├── 2. 从 DWD 取当前版本（scd2_is_current = 1）
    │
    ├── 3. 按自然键逐条比较：
    │       │
    │       ├── DWD 中不存在 → 收集为待插入（INSERT）
    │       │
    │       ├── 存在但 _is_row_changed() 返回 True → 收集为待更新
    │       │   ├── 关闭旧版本（scd2_end_time = now, scd2_is_current = 0）
    │       │   └── 插入新版本（scd2_start_time = now, scd2_is_current = 1, version + 1）
    │       │
    │       └── 存在且无变化 → 跳过（UNCHANGED）
    │
    ├── 4. _close_current_dim_bulk() — 批量关闭旧版本
    │
    └── 5. _insert_dim_rows_bulk() — 批量插入新版本
```

---

## 4. 维度表 SCD2 配置

> 跟踪字段 = 表中除自然键和 SCD2 控制列（`scd2_start_time`/`scd2_end_time`/`scd2_is_current`/`scd2_version`）之外的所有列。任一跟踪字段值变化即触发新版本。

### 4.1 门店维度（dim_site / dim_site_ex）

- Schema：`dwd`
- ODS 来源：`ods.table_fee_transactions`（从台费流水中的 `siteProfile` 快照提取）
- 自然键：`site_id`
- dim_site 跟踪字段：`org_id`、`tenant_id`、`shop_name`、`site_label`、`full_address`、`address`、`longitude`、`latitude`、`tenant_site_region_id`、`business_tel`、`site_type`、`shop_status`
- dim_site_ex 跟踪字段：`avatar`、`address`、`longitude`、`latitude`、`tenant_site_region_id`、`auto_light`、`light_status`、`light_type`、`light_token`、`site_type`、`site_label`、`attendance_enabled`、`attendance_distance`、`customer_service_qrcode`、`customer_service_wechat`、`fixed_pay_qrcode`、`prod_env`、`shop_status`、`create_time`、`update_time`
- 变更触发场景：门店名称/地址/状态/经纬度等基础信息变更

### 4.2 台桌维度（dim_table / dim_table_ex）

- Schema：`dwd`
- ODS 来源：`ods.site_tables_master`
- 自然键：`table_id`
- dim_table 跟踪字段：`site_id`、`table_name`、`site_table_area_id`、`site_table_area_name`、`tenant_table_area_id`、`table_price`、`order_id`
- dim_table_ex 跟踪字段：`show_status`、`is_online_reservation`、`table_cloth_use_time`、`table_cloth_use_cycle`、`table_status`、`create_time`、`light_status`、`tablestatusname`、`sitename`、`applet_qr_code_url`、`audit_status`、`charge_free`、`delay_lights_time`、`is_rest_area`、`only_allow_groupon`、`order_delay_time`、`self_table`、`temporary_light_second`、`virtual_table`
- 变更触发场景：台桌名称/区域/价格/状态变更

### 4.3 助教维度（dim_assistant / dim_assistant_ex）

- Schema：`dwd`
- ODS 来源：`ods.assistant_accounts_master`
- 自然键：`assistant_id`
- dim_assistant 跟踪字段：`user_id`、`assistant_no`、`real_name`、`nickname`、`mobile`、`tenant_id`、`site_id`、`team_id`、`team_name`、`level`、`entry_time`、`resign_time`、`leave_status`、`assistant_status`
- dim_assistant_ex 跟踪字段：`gender`、`birth_date`、`avatar`、`introduce`、`video_introduction_url`、`height`、`weight`、`shop_name`、`group_id`、`group_name`、`person_org_id`、`staff_id`、`staff_profile_id`、`assistant_grade`、`sum_grade`、`get_grade_times`、`charge_way`、`allow_cx`、`is_guaranteed`、`salary_grant_enabled`、`entry_type`、`entry_sign_status`、`resign_sign_status`、`work_status`、`show_status`、`show_sort`、`online_status`、`is_delete`、`criticism_status`、`create_time`、`update_time`、`start_time`、`end_time`、`last_table_id`、`last_table_name`、`last_update_name`、`order_trade_no`、`ding_talk_synced`、`site_light_cfg_id`、`light_equipment_id`、`light_status`、`is_team_leader`、`serial_number`、`system_role_id`、`job_num`、`cx_unit_price`、`pd_unit_price`
- 变更触发场景：助教等级/团队/状态/入职离职/评分等变更

### 4.4 会员维度（dim_member / dim_member_ex）

- Schema：`dwd`
- ODS 来源：`ods.member_profiles`
- 自然键：`member_id`
- dim_member 跟踪字段：`system_member_id`、`tenant_id`、`register_site_id`、`mobile`、`nickname`、`member_card_grade_code`、`member_card_grade_name`、`create_time`、`update_time`、`pay_money_sum`、`recharge_money_sum`、`birthday`
- dim_member_ex 跟踪字段：`referrer_member_id`、`point`、`register_site_name`、`growth_value`、`user_status`、`status`、`person_tenant_org_id`、`person_tenant_org_name`、`register_source`
- 变更触发场景：会员昵称/手机号/卡等级/累计消费充值/状态等变更

### 4.5 会员卡账户维度（dim_member_card_account / dim_member_card_account_ex）

- Schema：`dwd`
- ODS 来源：`ods.member_stored_value_cards`
- 自然键：`member_card_id`
- dim_member_card_account 跟踪字段：`tenant_id`、`register_site_id`、`tenant_member_id`、`system_member_id`、`card_type_id`、`member_card_grade_code`、`member_card_grade_code_name`、`member_card_type_name`、`member_name`、`member_mobile`、`balance`、`start_time`、`end_time`、`last_consume_time`、`status`、`is_delete`、`principal_balance`、`member_grade`
- dim_member_card_account_ex 跟踪字段：（60+ 列，含各类折扣比例、抵扣开关等，详见 DDL）
- 变更触发场景：卡余额/状态/折扣配置/有效期等变更

### 4.6 商品维度

#### 租户商品（dim_tenant_goods / dim_tenant_goods_ex）

- ODS 来源：`ods.tenant_goods_master`
- 自然键：`tenant_goods_id`
- dim_tenant_goods 跟踪字段：`tenant_id`、`supplier_id`、`category_name`、`goods_category_id`、`goods_second_category_id`、`goods_name`、`goods_number`、`unit`、`market_price`、`goods_state`、`create_time`、`update_time`、`is_delete`、`not_sale`

#### 门店商品（dim_store_goods / dim_store_goods_ex）

- ODS 来源：`ods.store_goods_master`
- 自然键：`site_goods_id`
- dim_store_goods 跟踪字段：`tenant_id`、`site_id`、`tenant_goods_id`、`goods_name`、`goods_category_id`、`goods_second_category_id`、`category_level1_name`、`category_level2_name`、`batch_stock_qty`、`sale_qty`、`total_sales_qty`、`sale_price`、`created_at`、`updated_at`、`avg_monthly_sales`、`goods_state`、`enable_status`、`send_state`、`is_delete`、`commodity_code`、`not_sale`

### 4.7 商品分类维度（dim_goods_category）

- ODS 来源：`ods.stock_goods_category_tree`
- 自然键：`category_id`
- 跟踪字段：`tenant_id`、`category_name`、`alias_name`、`parent_category_id`、`business_name`、`tenant_goods_business_id`、`category_level`、`is_leaf`、`open_salesman`、`sort_order`、`is_warehousing`
- 变更触发场景：分类名称/层级/排序/启用状态变更

### 4.8 团购套餐维度（dim_groupbuy_package / dim_groupbuy_package_ex）

- ODS 来源：`ods.group_buy_packages`
- 自然键：`groupbuy_package_id`
- dim_groupbuy_package 跟踪字段：`tenant_id`、`site_id`、`package_name`、`package_template_id`、`selling_price`、`coupon_face_value`、`duration_seconds`、`start_time`、`end_time`、`table_area_name`、`is_enabled`、`is_delete`、`create_time`、`tenant_table_area_id_list`、`card_type_ids`、`sort`、`is_first_limit`
- 变更触发场景：套餐名称/价格/面值/有效期/启用状态变更

### 4.9 员工维度（dim_staff / dim_staff_ex）

- ODS 来源：`ods.staff_info_master`
- 自然键：`staff_id`
- dim_staff 跟踪字段：`staff_name`、`alias_name`、`mobile`、`gender`、`job`、`tenant_id`、`site_id`、`system_role_id`、`staff_identity`、`status`、`leave_status`、`entry_time`、`resign_time`、`is_delete`
- dim_staff_ex 跟踪字段：`avatar`、`job_num`、`account_status`、`rank_id`、`rank_name`、`new_rank_id`、`new_staff_identity`、`is_reserve`、`shop_name`、`site_label`、`tenant_org_id`、`system_user_id`、`cashier_point_id`、`cashier_point_name`、`group_id`、`group_name`、`staff_profile_id`、`auth_code`、`auth_code_create`、`ding_talk_synced`、`salary_grant_enabled`、`entry_type`、`entry_sign_status`、`resign_sign_status`、`criticism_status`、`create_time`、`user_roles`
- 变更触发场景：员工姓名/岗位/角色/状态/入职离职等变更

---

## 5. 查询约定

### 获取当前有效记录

```sql
SELECT * FROM dwd.dim_member
WHERE scd2_is_current = 1;
```

### 获取某时间点的历史快照

```sql
SELECT * FROM dwd.dim_member
WHERE scd2_start_time <= '2025-06-01'
  AND scd2_end_time   >  '2025-06-01';
```

### 获取某记录的完整变更历史

```sql
SELECT * FROM dwd.dim_member
WHERE member_id = 12345
ORDER BY scd2_start_time;
```

---

## 6. 注意事项

- **时区**：`scd2_start_time` / `scd2_end_time` 使用 `TIMESTAMPTZ`，统一以 `Asia/Shanghai` 时区存储
- **并发安全**：当前实现在单次 ETL 运行内串行处理，未做行级锁；并发写入需额外保护
- **删除策略**：维度记录不做物理删除，仅通过关闭版本（`scd2_is_current = 0`）标记失效
- **ODS 来源过滤**：从 ODS 取数时统一使用 `DISTINCT ON (natural_key) ... WHERE is_delete IS DISTINCT FROM 1 ORDER BY natural_key, fetched_at DESC`，确保取最新有效版本

---

## 维护约定

- 新增维度表时，须在本文档添加对应章节
- 跟踪字段变更时，须同步更新文档并评估历史数据影响
- 文档统一 UTF-8 编码，中文撰写
