30-开发规范.md 30 KB

开发规范

本文档记录 poyee-data-warehouse 数仓数据开发流程与项目管理规范。 与 数仓命名规范.md90-重构路线.md 配合使用。

1. 项目管理规范(TPAD)

目前 TPAD 已创建数据需求工作流技术需求工作流

1.1 TPAD 任务建档要求

耗时 要求
2 小时以下临时需求 需要聊天记录做留档
2 小时以上 必须建立 TPAD 任务
8 小时以上 必须有方案或设计文档

2. 数据开发流程

2.1 数据开发全流程图

flowchart TD
    A[数据需求] --> B[需求沟通]
    B --> C[需求确认]
    C --> D{是否涉及敏感数据?}
    D -->|是| E[敏感数据使用审批]
    E --> F{是否审批通过?}
    F -->|否| Z[结束]
    F -->|是| G{是否需要新的采集方案?}
    D -->|否| G
    G -->|是| H[采集需求流程]
    H --> G
    G -->|否| I{所需指标维度<br/>是否已进仓?}
    I -->|否| J[数据接入流程]
    J --> I
    I -->|是| K[排期开发]
    K --> L[开发完成]
    L --> M[需求方验收]
    M --> N{是否验收通过?}
    N -->|否| K
    N -->|是| O{是否追溯历史数据?}
    O -->|否| Z
    O -->|是| P[追溯数据]
    P --> Q[追溯及验收完成]
    Q --> Z

2.2 关键节点说明

环节 说明
敏感数据审批 涉及用户身份证、联系方式、支付信息等敏感字段需走专项审批
采集需求流程 数据源尚未接入时,先走 DataX/Kafka 采集接入流程
数据接入流程 数据已采集但尚未建模入仓时,先落 ODS/DWD
指标维度复用判定 先检查所需指标是否已在字典,避免重复建模
数据探查 源数据行数、空值率、主键唯一性、字段分布
建模评审 对照总线矩阵与维度建模五步法(见 数仓分层与建模.md
命名合规自检 对照 数仓命名规范.md 第 7 节 Checklist
口径对齐 对照 指标体系.md 的指标字典
数据质量校验 在 DolphinScheduler 工作流中加入质量校验节点
历史回溯 确保任务支持按 dt 回跑;追溯完成后需要验收

3. 代码开发规范

3.1 Python / PySpark

  • PEP 8 风格
  • 禁用 dict.__contains__(key),改用 key in dict
  • 禁止 SQL 字符串拼接(防 SQL 注入),使用参数化查询
  • 硬编码配置项必须外置到 conf/(见 90-重构路线.md §2)
  • 敏感信息(数据库账密)不得入库
  • 新增 UDF 必须带注释与单元测试,再登记到 kb/31-UDF手册.md

3.2 SQL

  • 表名、字段名遵循五段式命名
  • 所有字段必须带 COMMENT
  • 分区字段统一 dt(日期)/ hr(小时)
  • 存储格式统一 ORC

3.2.1 为什么不对齐 AS

字段别名不对齐 AS 关键字是刻意的,理由:

  1. diff 噪音:对齐 AS 需要按同一 SELECT 内最长字段补空格。任意字段改名会触发整列空格重算,一个字段改动变成 N 行 diff,code review 里真实逻辑变更被空白变更淹没
  2. 与"逗号前置"冲突:逗号前置的动机就是让每一行独立、增删行不污染邻居;对齐 AS 又把"相邻行耦合"引回来了,自相矛盾
  3. git blame 失真:纯空白重排会把一堆行的作者改成最近那次格式化的人
  4. Hive/Spark SQL 字段常很长:数仓里 trd_order_pay_amt_rmb_total_1d 这种 30+ 字符的字段名很常见,对齐后右边要留 40+ 空格,一屏横向放不下反而更难读
  5. 可读性不需要靠对齐:每个字段一行 + 逗号前置已足够清晰

3.3 Shell

  • 环境变量统一从 conf/env.sh 读取
  • 避免在业务脚本中重复环境检测逻辑(统一交给 Python 入口)

3.4 Git 协作规范

本节定义数仓项目团队使用 Git 协作的分支模型与工作流程,保证主干历史整洁可追溯,降低多人协作冲突风险。

3.4.1 分支模型

3.4.1.1 分支总览
分支 定位 受保护 合并来源 触发动作
master 稳定版本归档 release 打 Tag(里程碑、冒烟通过等)
release 线上运行版本 feature 发布到线上
feature 公共开发分支 feature-xxx(个人分支) 测试通过后合入 release
feature-xxx 个人开发分支 —— 通过 PR 合入 feature 后自动删除
3.4.1.2 分支结构
                                     [tag: datax+spark-smoke-2026-04-20]
                                              │
  master  ──────────────────────────●─────────────────────▶  (稳定版本归档)
                                   ↗
                                  ↗  merge (里程碑 / 冒烟通过)
                                 ↗
  release ────●────●────●──────●──────────────────────────▶  (线上版本)
              ↑    ↑    ↑
              │    │    │  merge (测试通过后发布)
              │    │    │
  feature ─●──●─●──●─●──●──────────────────────────────────▶  (公共开发)
           ↑    ↑    ↑
           │    │    │  PR + Review (管理员合并)
           │    │    │
           │    │    └── feature-lisi-dwd-trd-20260418    ✗ (PR 合入后自动删除)
           │    └────── feature-wangwu-dim-shp-20260419   ✗ (PR 合入后自动删除)
           └─────────── feature-zhangsan-ods-usr-20260420 ✗ (PR 合入后自动删除)
3.4.1.3 保护策略

masterreleasefeature 三个分支在仓库层面禁用远程推送,所有变更必须通过 PR(Pull Request / Merge Request)流转,仅允许管理员通过合并操作更新。

3.4.2 代码流转路径

flowchart LR
    A[feature-xxx<br/>个人分支] -->|PR + Review| B[feature<br/>公共开发]
    B -->|测试通过<br/>线上发布| C[release<br/>线上版本]
    C -->|里程碑/冒烟通过<br/>打 Tag| D[master<br/>稳定归档]

代码单向汇聚到 master,各合并节点由管理员操作,releasemaster 的合并提交需打 Tag。

3.4.3 开发者工作流

sequenceDiagram
    participant Dev as 开发者
    participant Local as 本地仓库
    participant Remote as 远端仓库
    participant Admin as 管理员

    Dev->>Remote: fetch origin
    Dev->>Local: 基于最新 origin/feature 新建个人分支
    Note over Dev,Local: 每次新任务必须重新拉取 feature 建分支

    loop 日常开发
        Dev->>Local: 编码 + commit
    end

    Note over Dev,Remote: 提 PR 前必须同步远端 feature
    Dev->>Remote: fetch origin feature
    Dev->>Local: rebase origin/feature

    alt 有冲突
        Dev->>Local: 本地解决冲突后继续 rebase
    end

    Dev->>Remote: push --force-with-lease 推送个人分支
    Dev->>Remote: 网页端发起 PR

    Admin->>Remote: Code Review
    alt Review 通过
        Admin->>Remote: 合并 PR 到 feature
        Remote-->>Remote: 自动删除 feature-xxx
    else 需要修改
        Admin-->>Dev: 提出修改意见
        Dev->>Local: 修改后重复 rebase + 强推
    end

关键约束

  • 新建分支与提 PR 前必须先 fetch 远端,以 origin/feature 的最新状态为基准,禁止以本地 feature 为基准(本地 feature 可能滞后于远端)
  • 个人分支必须基于最新 origin/feature 创建,禁止复用已合并 PR 的旧个人分支
  • 提 PR 前必须 rebase origin/feature,冲突在本地解决后继续 rebase
  • 强推一律使用 --force-with-lease,禁用 --force
  • PR 合并后远端个人分支自动删除,下次任务重新 fetch 后基于 origin/feature 建新分支

日常通过 PyCharm 的 Git 面板与仓库网页端完成上述操作即可。

3.4.4 要求使用 Rebase 而非 Merge

  • 历史线性整洁:没有冗余的 merge 提交,可视化合并树清晰
  • 二分查找友好git bisect 定位问题提交更精准
  • Review 聚焦:PR 里只有本次任务的提交,Reviewer 不会看到与本次无关的 merge commit

3.4.5 Hotfix 紧急修复流程

线上 release 分支出现紧急故障时,走独立的 hotfix 流程,避免等待 feature 中未完成功能的流转。

3.4.5.1 流转路径
flowchart LR
    A[release<br/>线上版本] -->|基于 release 拉出| B[hotfix-xxx<br/>修复分支]
    B -->|PR #1 + Review| A
    B -->|PR #2 + Review| D[feature<br/>公共开发]
    A -->|修复验证通过<br/>打 hotfix tag| C[master<br/>稳定归档]

关键点:同一个 hotfix-xxx 分支需要发起两个 PR,分别合入 releasefeature,保证后续开发基于已修复的代码。两个 PR 都合并后再删除 hotfix 分支。

为什么不是 release 合入 featurerelease 本身由大量 merge commit 构成,反向合入会污染 feature 的线性历史。用 hotfix 分支发两个 PR,两边拿到的都是同一个独立的修复提交。

3.4.5.2 操作步骤
  1. 开发者基于最新 origin/release 创建 hotfix-xxx 分支,只做 bug 修复,不夹带其他改动
  2. 提 PR #1release,管理员 Review 通过后合入,暂不删除 hotfix 分支
  3. 管理员 / QA 验证修复并部署线上
  4. 开发者基于同一 hotfix 分支提 PR #2feature
  5. 管理员 Review 通过后合入 feature,删除 hotfix 分支
  6. 管理员将 release 合入 master 并打 hotfix tag
3.4.5.3 冲突异常处理

正常情况下 PR #2 不应产生冲突——hotfix 职责单一,feature 上不应存在针对同一代码的并行修改。

若产生冲突,说明 feature 上有人改动了 hotfix 修复的同一区域。此时不要自行解决冲突,否则会出现 releasefeature 修复版本不一致,后续发布可能导致 bug 复现。正确做法:暂停合并,由管理员召集 hotfix 作者与 feature 侧的改动者协调,确认最终版本后再继续。

3.4.6 分支 / Tag 命名

3.4.6.1 个人分支命名

格式:feature-<姓名拼音>-<描述>-<日期>

示例 含义
feature-lisi-dwd-trd-20260418 李四开发交易域 DWD 层,2026-04-18 建分支
feature-wangwu-dim-shp-20260419 王五开发商品维度,2026-04-19 建分支
feature-zhangsan-ods-usr-20260420 张三开发用户域 ODS 层,2026-04-20 建分支
3.4.6.2 Hotfix 分支命名

格式:hotfix-<姓名拼音>-<故障简述>-<日期>

示例 含义
hotfix-zhangsan-ods-usr-duplicate-20260421 修复用户域 ODS 层数据重复
hotfix-lisi-ads-prd-null-20260422 修复产品宽表空值异常
hotfix-wangwu-dws-mkt-stale-20260423 修复营销宽表数据滞后
3.4.6.3 Tag 命名

格式:<描述>-<类型>-<日期>

示例 含义
datax+spark-smoke-2026-04-20 DataX + Spark 链路冒烟测试通过
dim-calendar-smoke-2026-04-18 dim_calendar 维度表冒烟通过
ods-usr-duplicate-hotfix-2026-04-21 ODS 用户域数据重复修复上线

规则

  • 阶段类型:smoke(冒烟通过)、hotfix(紧急修复)
  • Tag 打在 releasemaster 的合并提交上,由管理员操作

3.4.7 Commit 信息(Conventional Commits)

每条 commit message 必须以 <type>: <简短描述> 开头,type 从下面固定列表里选一个;长说明放正文,与标题空一行。

type 用途 示例
feat 新功能 / 新 job / 新表 feat(raw/crm): 新增 ods_crm_ent_contact_di
fix Bug 修复 / 数据订正 fix(dwd/trd): 修复订单金额币种换算错误
docs 只改文档(含 kb/、注释、README) docs(kb): 更新 21-命名规范.md §3.4 示例
refactor 不改变外部行为的重构 refactor(dw_base): tendata → dw_base 模块改名
perf 性能优化 perf(dws): 拆 tmp 表减少 shuffle
test 只增/改测试 test(udf): 补 safe_cast_decimal 边界用例
chore 构建、依赖、CI、打包 chore: 精简 requirements.txt
style 空白 / 格式 / import 顺序(不改逻辑) style: 统一 SQL 字段缩进
build 打包脚本 / publish.sh / Dockerfile build: publish.sh 支持 -env 参数
ci DolphinScheduler / GitHub Actions 配置 ci: DS 工作流加质量校验节点
ops 运维类操作(补数、回刷、重跑、人工干预) ops(dwd/trd): 补 20260101-20260131 订单分区
revert 撤销某次提交 revert: feat(raw/crm): ...

约定

  1. 标题 ≤ 50 字符,祈使句(「新增 xxx」而不是「新增了 xxx」)
  2. scope 可选但推荐:数仓项目里 scope 常取 {层}/{域}(如 dwd/trd)或模块名(dw_basebinkbconf
  3. 破坏性变更:标题末尾加 !,正文以 BREAKING CHANGE: 开头说明迁移方式
    • 例:refactor(dw_base)!: 拆分 __init__.py + BREAKING CHANGE: 需要在调用处显式 import findspark
  4. 一次提交做一件事:不要把"新增表 + 顺手修 bug + 改文档"塞一起,按 type 拆成 3 个 commit
  5. 关联 TPAD / issue:正文末尾加 Refs: TPAD-1234Closes: #42

反例(会被 reject):

  • update / 修改 / 提交 —— 没有 type,描述空洞
  • feat: xxx fix: yyy docs: zzz —— 一条 commit 混多个 type
  • feat: 新增了一大堆表 —— scope 和具体目标不明

4. 数仓开发文件组织

讲 DDL 与计算 SQL 怎么在 jobs/manual/ddl/ 下组织;本节管"文件放哪",§3 管"代码怎么写"。

核心原则:DDL 与计算 SQL 物理分离,DDL 全部在 manual/ddl/ 下单一来源。

  • manual/ddl/ 存放所有 DDL(首次建表 + 后续 ALTER),采用 migration 模式:每次 DDL 操作是一个不可变文件,禁止回头改老文件
  • jobs/ 存放调度执行的采集 / 计算任务,只做 INSERT OVERWRITE 或数据同步,不写 CREATE TABLE

一张表的完整生命周期涉及:

  • manual/ddl/{layer}/{domain}/{表名}_create.sql —— 首次建表,永久保留
  • 若干 manual/ddl/{layer}/{domain}/{表名}_{yyyymmdd}_{描述}_change.sql —— 之后每次 ALTER,独立文件
  • jobs/{layer}/{domain}/{表名}.sqljobs/{layer}/{domain}/{表名}/{表名}-{NN}-{描述}.sql —— 调度执行的计算 SQL(不含建表),详见 §4.2

4.1 manual/ddl/ —— DDL 唯一来源

目录组织:按 {layer}/{domain}/ 分子目录。layer 代码取自 21-命名规范.md §3.1(raw/ods/dim/dwd/dws/tdm/ads),domain 代码取自 §3.2(trd/usr/prd/shp/pub)。每张目标表的首次建表 + 所有 ALTER 都落在这个子目录里,便于一眼看清某层某域的表清单。

manual/ddl/
├── raw/
│   └── trd/
│       ├── raw_trd_order_pay_inc_d_create.sql           # 首次建表(永久保留)
│       └── 20260612_raw_trd_legacy_order_change_partition.sql
├── ods/
│   └── trd/
│       └── ods_trd_order_pay_inc_d_create.sql
├── dwd/
│   └── trd/
│       ├── dwd_trd_order_pay_inc_d_create.sql
│       └── 20260520_dwd_trd_order_pay_add_refund.sql    # ALTER(独立文件,不改原文件)
├── ads/
│   └── trd/
│       └── ads_trd_gmv_d_create.sql
├── tmp/                                                 # 单目标加速中间表 DDL(见 §4.2)
│   └── dwd_trd_order_pay/
│       ├── tmp_dwd_trd_order_pay_01_create.sql
│       └── tmp_dwd_trd_order_pay_02_create.sql
└── archive/
    └── 20260301_old_alter.sql                           # 已归档

grep 的友好度grep -r "CREATE TABLE.*dwd_trd_order_pay_inc_d" manual/ddl/ 仍能直接命中;分子目录带来的额外索引成本小于"一眼看到分层分域"的收益。

存储格式约定:所有分层一律 STORED AS ORC。策略详见 20-数仓分层与建模.md §7。

4.2 jobs/ 层 —— 调度执行的计算 SQL

文件粒度:一张目标表对应一套 SQL 文件,按复杂度两档:

  • 简单表jobs/{layer}/{domain}/{表名}.sql 一个文件顶到底(单次 INSERT OVERWRITE,可带 WITH CTE)
  • 多步表jobs/{layer}/{domain}/{表名}/{表名}-{NN}-{描述}.sql,序号三位,99 固定留给最终 INSERT OVERWRITE 目标表那一步。DS 工作流对应 N 个 task 节点按序号链式依赖

所有 .sql 只写 INSERT OVERWRITE / INSERT INTO不写 CREATE TABLE(表由 manual/ddl/ 保证已存在)。

jobs/dwd/trd/
├── dwd_trd_order_refund_inc_d.sql            # 简单表,单文件
├── dwd_trd_shop_gmv_agg_ful_d.sql
└── dwd_trd_order_pay_inc_d/                  # 多步表,目标表名同名子目录
    ├── dwd_trd_order_pay_inc_d-01-build_tmp_pay_base.sql
    ├── dwd_trd_order_pay_inc_d-02-build_tmp_refund_agg.sql
    └── dwd_trd_order_pay_inc_d-99-insert_target.sql

什么时候从简单表升级到多步表:

触发条件 处理
单 SQL shuffle 过大(单作业耗时 > 30 min 且 shuffle read > 100GB) 拆分中间结果物化为 tmp 表
同一块 CTE 在多个 WITH 节里重复扫描 物化后 cache 复用
复杂业务逻辑,读多源后多轮 join,需要中间落盘便于 debug / 回溯 拆分单步
中间结果需要被多个目标表复用 不用 tmp,升层为 dwd/dws 独立表

中间表两类,严格区分:

  1. 单目标加速中间表(tmp) — 只服务本目标表,命名 tmp_{目标表名}_{NN},DDL 收到 manual/ddl/tmp/{目标表名}/ 子目录。生命周期跟随本次任务,每次 INSERT OVERWRITE 覆盖或 drop+recreate,不留历史
  2. 可复用中间结果 — 被 ≥2 个目标表引用,升层为独立 dwd/dws 表,按正常五段式命名,DDL 单独登记。不允许用 tmp 前缀

从单文件升级到子目录的操作步骤:删掉原单文件,建子目录、拆 SQL、DS 工作流拆 task 节点;manual/ddl/tmp/{目标表名}/ 同步补齐 tmp 表 DDL。一次性改完,避免半新半旧。

WITH / CTE 还是拆文件:轻量中间结果用 WITH 内联(不物化,本质还是单 SQL);重量中间结果需要物化为 tmp 表时才升级到"多步表子目录"(见本节上方触发条件表)。不要盲目把 CTE 都拆成 tmp —— shuffle 不大、不复用的 CTE 留在 WITH 里反而更清爽。

4.3 raw 层(采集任务)

raw 层的 jobs/ 有两类主要任务,根据源数据形态选择:

场景 文件类型 执行器
从 MongoDB / PG / MySQL 等结构化源库同步 .ini(DataX 配置) bin/datax-single-job-starter.sh
从本地 / 外部 CSV 文件导入 .sql(含 USING csv 临时视图 + INSERT OVERWRITE bin/csv-to-hdfs-starter.py(阶段 1 实现)

raw 层数据类型约定:全字段 STRING,类型转换与脏数据识别下推到 ods 层。契约详见 20-数仓分层与建模.md §8.1。

DataX ini 引用数据源约定:sync ini 里 [reader] / [writer]dataSource 字段必须写成 {db_type}/{env}-{实例简称}(例如 postgresql/prod-hobbyhdfs/prod-ha),指向项目同级目录 datasource/{db_type}/{env}-{实例简称}.ini。裸名(如 hobby)无法解析。代码按 / 切首段取 db_type(即父目录名),实现在 dw_base/datax/plugins/plugin.py:37plugin_factory.py:34。跨环境同步(如 test 业务库 → prod HDFS)是常态,不设全局 env 概念,每个 sync ini 显式指向各自 env 的 source ini。

CSV 导入流程

  1. 本地 CSV 文件如果较大,先 gzip 压缩
  2. bin/csv-to-hdfs-starter.py 把(压缩后的)CSV hdfs dfs -put 到 HDFS 暂存区
  3. 调用 SparkSQL 执行 jobs/raw/{域}/{表}.sql,文件内通过 USING csv OPTIONS(...) 临时视图解析 CSV,再 INSERT OVERWRITE 写入对应 raw 表
  4. 清理 HDFS 暂存文件

raw 层写入模式对照

场景 写法 manual/ddl/
一次性 CSV 导入(历史回刷、单批 vendor 数据),表名 raw_xxx_his_o 预建 EXTERNAL TABLE(不分区),INSERT OVERWRITE TABLE ... 需要
每日重复的 CSV 导入(daily file drop) 预建分区 EXTERNAL TABLE,每日 INSERT OVERWRITE TABLE ... PARTITION (dt='${dt}') 需要
结构化源库同步(PG/MySQL 等) DataX ini,写入预建 EXTERNAL TABLEwriteMode=truncate 或分区覆盖) 需要

his 表为什么不分区:一次性导入永不追加,分区裁剪没有意义。下游 ods 再按 dt 分区,一次性切片。

为什么用 SQL 而不是 YAML 描述 CSV 任务

  • 复用 SparkSQL 现有执行链,bin/csv-to-hdfs-starter.py 只需在 bin/spark-sql-starter.py 之外加一层 gzip+put+清理的薄壳,不需要单独的 YAML 渲染器
  • USING csv OPTIONS(...) 本身就是 Spark 的声明式 CSV 读取语法,YAML 再封装一层是多余的
  • 与其他分层文件类型一致(除 raw DataX ini 外,其他都是 .sql),读者不需要切换上下文

4.4 ads 层(SQL + 导出 ini 并存)

manual/ddl/
└── ads_trd_gmv_d.sql                  # 建表 DDL(首次建表,永久保留)

jobs/ads/trd/
├── ads_trd_gmv_d.sql                  # 每日计算产出 ads 表
└── ads_trd_gmv_d_export.ini           # 导出到 Doris/ClickHouse/MySQL 的 DataX ini

命名规则:导出 ini 文件名 = {ads 表名}_export.ini,便于一眼对应。

4.5 文件命名速查

目录 文件后缀 文件名规则 说明
manual/ddl/{layer}/{domain}/ .sql {表名}_create.sql(首次) 或 {yyyymmdd}_{表名}_{change}.sql(ALTER) DDL 唯一来源;首次建表用 CREATE TABLE IF NOT EXISTS,后续 ALTER 带日期前缀
manual/ddl/tmp/{目标表名}/ .sql tmp_{目标表名}_{NN}_create.sql 多步表的单目标加速中间表 DDL
jobs/raw/{domain}/ .ini(DataX)或 .sql(CSV 导入) {目标表名}.ini{目标表名}.sql DataX 采集或 CSV 导入任务定义
jobs/{ods\|dwd\|dws\|tdm}/{domain}/ .sql 简单表{目标表名}.sql多步表:子目录 {目标表名}/{目标表名}-{NN}-{描述}.sql99 为最终 insert) 每日 INSERT OVERWRITE 计算,详见 §4.2
jobs/ads/{domain}/ .sql + .ini 简单表{ads 表名}.sql + {ads 表名}__{db_type}_{instance}.ini多步{ads 表名}/{ads 表名}-{NN}-{描述}.sql + 同级目录放 ini 产出 + 导出;同一张 ads 表扇出多下游时各一份 ini
manual/backfill/ .sql {yyyymmdd}_{表名}_history.sql 一次性历史回刷脚本
manual/imports/{yyyymmdd}/ .ini / .sql {任务描述}.ini.sql 一次性入仓任务(离线硬盘、历史 dump、外部 CSV 等),按执行日期归档
manual/exports/{yyyymmdd}/ .ini {任务描述}.ini 一次性出仓任务,按执行日期归档

4.6 表结构变更流程(migration 模式)

当要给某张表加列 / 改字段时,只写新文件,不改老文件

manual/ddl/{yyyymmdd}_{表名}_{change}.sql 写 ALTER 语句(带工单号、目的、回滚方案)

  • ALTER 文件按时间前缀线性堆叠,grep dwd_trd_order_pay manual/ddl/ 即可看到该表的全部 DDL 历史,按文件名时间序回放就是表结构的完整演化
  • 真要在新环境重建这张表,按时间顺序把 manual/ddl/{表名}.sql + 所有相关 ALTER 文件依次执行即可,结果和生产一致。注意:目前没有自动化重放工具,需要人手按文件名时间序执行;未来视需要可以写一个 bin/replay-ddl.sh(当前未实现)
  • 这是数据库 migration 工具(Flyway / Alembic / Liquibase)的标准做法,已被工业界验证

5. 测试规范

90-重构路线.md §6。核心要点:

  • UDF 单测:纯 Python,不依赖 Spark
  • DataX 配置生成单测
  • Spark 集成测试:local[*] 模式
  • 数据质量校验:行数、空值率、主键唯一性

6. manual/ 临时 SQL 规范

manual/ 目录存放一次性、非幂等的 SQL 脚本,与 jobs/ 的语义完全独立。子目录树见 00-项目架构.md §1。开发团队核心规则:

  1. 严禁接入定时调度manual/ 下任何脚本都不得被 DolphinScheduler 定时工作流引用;仅允许通过一次性工作流或命令行手动触发
  2. 命名必须带日期前缀{yyyymmdd}_{层}_{域}_{简述}.sql,便于按时间排序与过期归档
  3. 文件头必须声明元信息:作者、日期、工单号、目的、执行状态(待执行 / 已执行 yyyy-mm-dd)
  4. fix/backfill/ 强制 Review:涉及线上数据订正和历史回刷的脚本,合并前至少 1 人 Review
  5. jobs/ 的边界
    • 表结构变更 → 只在 manual/ddl/ 写一个新的 ALTER 文件不要回头改 manual/ddl/{表名}.sql(migration 模式,详见 §4.6)。
    • 历史数据回刷 → 优先复用 jobs/ 原 SQL + 日期参数,manual/backfill/ 只放调用包装
    • 临时取数给业务方 → manual/adhoc/
    • 脏数据订正 → manual/fix/,必须附 TPAD 工单号

7. 开发样板

conf/templates/ 下按引擎分顶层,模板供开发者照抄写新文件,不被任何代码读取。

类别 目录 说明
DataX 数据源连接 conf/templates/datax/datasource/ 源 ini 样板(已备 postgresql / hdfs / hdfs-ha),对应项目根 datasource/{db_type}/{env}-{实例简称}.ini
DataX 同步作业 conf/templates/datax/sync/ sync ini 样板,对应 jobs/raw/ / jobs/ads/ / manual/ 下的 DataX 作业
Spark SQL 作业 conf/templates/spark/sql/ 各层 INSERT OVERWRITE 样板
Spark 建表 DDL conf/templates/spark/ddl/ 各层 CREATE EXTERNAL TABLE ... STORED AS ORC 样板

约定

  • 文件名用双扩展名 *.template.{ini,sql},避免被 DataX / Spark SQL 引擎误拾
  • 新增样板归到对应子目录;新增引擎类时顶层并列目录

8. manual/ 目录执行规范

定位:一次性、非幂等的 SQL 脚本;与 jobs/ 语义完全独立,严禁接入 DolphinScheduler 定时调度

子目录职责

目录 用途
manual/ddl/ 所有 DDL(初始 CREATE + 后续 ALTER),唯一来源;内部按 {layer}/{domain}/ 分子目录
manual/backfill/ 历史数据回刷(跨日期重算)
manual/fix/ 线上脏数据订正,必须附工单号
manual/adhoc/ 临时取数、问题排查
manual/imports/{yyyymmdd}/ 一次性入仓任务(离线硬盘、历史 dump、外部 CSV 等),按执行日期归档
manual/exports/{yyyymmdd}/ 一次性出仓任务,按执行日期归档
manual/archive/ 执行完毕的历史脚本归档,保留审计痕迹

命名规则{yyyymmdd}_{层}_{域}_{简述}.sql,例如 20260414_dwd_trd_add_refund_col.sql

文件头强制注释

-- 作者:xxx
-- 日期:2026-04-14
-- 工单:TPAD-1234
-- 目的:补录 2026-Q1 的退款维度
-- 状态:[待执行 | 已执行 2026-04-14]

dataxini

sync ini 里 [reader] / [writer]dataSource 字段必须带 {db_type}/ 前缀,例如 dataSource = postgresql/prod-hobbydataSource = hdfs/prod-ha。代码按首段斜杠判 db_type(= 父目录),裸名(hobby)会找不到文件。

-- 作者:xxx -- 日期:2026-04-14 -- 工单:TPAD-1234 -- 目的:补录 2026-Q1 的退款维度 -- 状态:[待执行 | 已执行 2026-04-14]

执行与回收

  • 执行入口复用 bin/spark-sql-starter.py,不新增脚本
  • 仅通过 DS 一次性工作流或命令行手动触发
  • fix/backfill/ 类脚本上线前必须经过 1 人以上 Review

6.3 DataX ini 配置格式

  1. RDBMS reader 的 columnType 当前被完全忽略PostgreSQLReader.load_columnpostgresql_reader.py:74-76)、MySQLReaderClickHouseReader 都覆盖了基类 Plugin.load_column,只读 column(字段名列表),columnType 不解析;类型靠 JDBC 驱动的 ResultSetMetaData 返回。对应的 writer 同样只读 column只有 HDFS/HBase/Kafka 这类读写文件/非关系型存储的插件走基类 Plugin.load_columnplugin.py:63-118),此时 columnType 才生效,且字符串字段可省略(基类默认类型是 string,见 plugin.py:77)。这一条与 kb/20 §8.1 raw 层"DataX ini 不写类型映射"的约定方向一致,但底层机制是上游代码覆盖掉了,不是约定的结果。

增量/全量区分:

  • dt=19700101query={} → 全量
  • query 中含 ${start_date}/${stop_date} → 增量

8. 相关文档