# 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 编码,中文撰写