# 开发规范 > 本文档记录 `poyee-data-warehouse` 数仓数据开发流程与项目管理规范。 > 与 `数仓命名规范.md`、`90-重构路线.md` 配合使用。 ## 1. 项目管理规范(TPAD) 目前 TPAD 已创建**数据需求工作流**和**技术需求工作流**。 ### 1.1 TPAD 任务建档要求 | 耗时 | 要求 | |------|------| | **2 小时以下**临时需求 | 需要聊天记录做留档 | | **2 小时以上** | 必须建立 TPAD 任务 | | **8 小时以上** | 必须有方案或设计文档 | ## 2. 数据开发流程 ### 2.1 数据开发全流程图 ```mermaid flowchart TD A[数据需求] --> B[需求沟通] B --> C[需求确认] C --> D{是否涉及敏感数据?} D -->|是| E[敏感数据使用审批] E --> F{是否审批通过?} F -->|否| Z[结束] F -->|是| G{是否需要新的采集方案?} D -->|否| G G -->|是| H[采集需求流程] H --> G G -->|否| I{所需指标维度
是否已进仓?} 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 保护策略 `master`、`release`、`feature` 三个分支在仓库层面禁用远程推送,所有变更必须通过 PR(Pull Request / Merge Request)流转,仅允许管理员通过合并操作更新。 #### 3.4.2 代码流转路径 ```mermaid flowchart LR A[feature-xxx
个人分支] -->|PR + Review| B[feature
公共开发] B -->|测试通过
线上发布| C[release
线上版本] C -->|里程碑/冒烟通过
打 Tag| D[master
稳定归档] ``` 代码单向汇聚到 `master`,各合并节点由管理员操作,`release` → `master` 的合并提交需打 Tag。 #### 3.4.3 开发者工作流 ```mermaid 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 流转路径 ```mermaid flowchart LR A[release
线上版本] -->|基于 release 拉出| B[hotfix-xxx
修复分支] B -->|PR #1 + Review| A B -->|PR #2 + Review| D[feature
公共开发] A -->|修复验证通过
打 hotfix tag| C[master
稳定归档] ``` **关键点**:同一个 `hotfix-xxx` 分支需要发起**两个 PR**,分别合入 `release` 和 `feature`,保证后续开发基于已修复的代码。两个 PR 都合并后再删除 hotfix 分支。 > 为什么不是 `release` 合入 `feature`?`release` 本身由大量 merge commit 构成,反向合入会污染 `feature` 的线性历史。用 hotfix 分支发两个 PR,两边拿到的都是同一个独立的修复提交。 ##### 3.4.5.2 操作步骤 1. 开发者基于最新 `origin/release` 创建 `hotfix-xxx` 分支,**只做 bug 修复,不夹带其他改动** 2. 提 PR #1 至 `release`,管理员 Review 通过后合入,**暂不删除 hotfix 分支** 3. 管理员 / QA 验证修复并部署线上 4. 开发者基于同一 hotfix 分支提 PR #2 至 `feature` 5. 管理员 Review 通过后合入 `feature`,删除 hotfix 分支 6. 管理员将 `release` 合入 `master` 并打 hotfix tag ##### 3.4.5.3 冲突异常处理 正常情况下 PR #2 不应产生冲突——hotfix 职责单一,`feature` 上不应存在针对同一代码的并行修改。 **若产生冲突,说明 `feature` 上有人改动了 hotfix 修复的同一区域**。此时**不要自行解决冲突**,否则会出现 `release` 与 `feature` 修复版本不一致,后续发布可能导致 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 打在 `release` → `master` 的合并提交上,由管理员操作 #### 3.4.7 Commit 信息(Conventional Commits) 每条 commit message 必须以 `: <简短描述>` 开头,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_base`、`bin`、`kb`、`conf`) 3. **破坏性变更**:标题末尾加 `!`,正文以 `BREAKING CHANGE:` 开头说明迁移方式 - 例:`refactor(dw_base)!: 拆分 __init__.py` + `BREAKING CHANGE: 需要在调用处显式 import findspark` 4. **一次提交做一件事**:不要把"新增表 + 顺手修 bug + 改文档"塞一起,按 type 拆成 3 个 commit 5. **关联 TPAD / issue**:正文末尾加 `Refs: TPAD-1234` 或 `Closes: #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}/{表名}.sql` 或 `jobs/{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-hobby`、`hdfs/prod-ha`),指向项目同级目录 `datasource/{db_type}/{env}-{实例简称}.ini`。裸名(如 `hobby`)无法解析。代码按 `/` 切首段取 db_type(即父目录名),实现在 `dw_base/datax/plugins/plugin.py:37`、`plugin_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 TABLE`(`writeMode=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}-{描述}.sql`(`99` 为最终 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/{layer}/{domain}/{yyyymmdd}_{表名}_{change}.sql` 写 ALTER 语句(带工单号、目的、回滚方案) - ALTER 文件按时间前缀线性堆叠,`grep dwd_trd_order_pay manual/ddl/` 即可看到该表的全部 DDL 历史,按文件名时间序回放就是表结构的完整演化 - 真要在新环境重建这张表,按时间顺序把 `manual/ddl/{layer}/{domain}/{表名}_create.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` **文件头强制注释**: ```sql -- 作者:xxx -- 日期:2026-04-14 -- 工单:TPAD-1234 -- 目的:补录 2026-Q1 的退款维度 -- 状态:[待执行 | 已执行 2026-04-14] ``` dataxini sync ini 里 `[reader]` / `[writer]` 的 `dataSource` 字段必须带 `{db_type}/` 前缀,例如 `dataSource = postgresql/prod-hobby`、`dataSource = 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_column`(`postgresql_reader.py:74-76`)、`MySQLReader`、`ClickHouseReader` 都覆盖了基类 `Plugin.load_column`,只读 `column`(字段名列表),`columnType` 不解析;类型靠 JDBC 驱动的 `ResultSetMetaData` 返回。对应的 writer 同样只读 `column`。**只有 HDFS/HBase/Kafka 这类读写文件/非关系型存储的插件**走基类 `Plugin.load_column`(`plugin.py:63-118`),此时 `columnType` 才生效,且字符串字段可省略(基类默认类型是 `string`,见 `plugin.py:77`)。这一条与 kb/20 §8.1 raw 层"DataX ini 不写类型映射"的约定方向一致,但底层机制是上游代码覆盖掉了,不是约定的结果。 **增量/全量区分:** - `dt=19700101` 或 `query={}` → 全量 - `query` 中含 `${start_date}`/`${stop_date}` → 增量 ## 8. 相关文档 - [命名规范](21-命名规范.md) - [数仓分层与建模](20-数仓分层与建模.md) - [重构路线](90-重构路线.md)