# 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. 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. 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. SET scd2_end_time = , scd2_is_current = 0 WHERE COALESCE(scd2_is_current, 1) = 1 AND "" = ANY() -- 复合主键:逐条 execute_batch UPDATE billiards_dwd.
SET scd2_end_time = , scd2_is_current = 0 WHERE COALESCE(scd2_is_current, 1) = 1 AND "" = %s AND "" = %s ``` **步骤 B — 批量插入新版本**(`_insert_dim_rows_bulk()`): ```sql INSERT INTO billiards_dwd.
(<所有列>) 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": , "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.
(<列>) 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 >= AND fetched_at < ` | | 2 | 无手动窗口 | 自动计算水位线 `_get_fact_watermark()`,`WHERE fetched_at > ` | `_get_fact_watermark()` 的计算逻辑: - 若 DWD 表包含 `fetched_at` 列 → `SELECT MAX(fetched_at) FROM ` - 若 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("" AS text), '') AS )` #### 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 () DO UPDATE SET ... WHERE <任一列 IS DISTINCT FROM>` | | `fact_upsert=False` 且无 `content_hash` | `ON CONFLICT () 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 (<列>) SELECT <列> FROM o LEFT JOIN d ON d. = o. WHERE d. IS NULL AND o.fetched_at > -- 同样受水位线约束 ON CONFLICT () 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"."" -- ODS 端 SELECT COUNT(1) AS cnt FROM "billiards_ods"."" ``` 返回结构: ```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(""), 0) AS val FROM "billiards_dwd"."" -- ODS 端 SELECT COALESCE(SUM(""), 0) AS val FROM "billiards_ods"."" ``` 返回结构(每个金额列一条记录): ```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`)