3. 版本控制与分支策略

Snowflake DataOps 的持续交付,本质上是在“数据库对象”和“代码资产”之间建立可追溯、可回滚、可审计的映射关系。Snowflake 本身没有像应用代码那样的 Git 分支能力,因此版本控制必须由外部 Git 仓库承担,而 Snowflake 侧只保留“当前生效状态”。理解这一点,是设计仓库结构和分支模型的前提。

一、Git 仓库结构:按对象类型分层,而非按环境分层

常见的错误做法是按 dev、test、prod 建三个目录,把同一份 DDL 复制三份。这样做的后果是:任何一次字段变更都要改三处,且三处极易漂移。正确做法是按“对象类型 + 业务域”组织,环境差异通过参数化或环境变量注入。

一个可直接参考的仓库结构如下:

snowflake-dataops/
├── databases/
│   └── sales_dw/
│       ├── schemas/
│       │   ├── raw.sql
│       │   ├── staging.sql
│       │   └── mart.sql
│       ├── tables/
│       │   ├── stg_orders.sql
│       │   └── dim_customer.sql
│       ├── views/
│       │   └── v_order_summary.sql
│       └── streams_tasks/
│           └── task_load_orders.sql
├── warehouses/
│   └── wh_etl.sql
├── roles_grants/
│   ├── roles.sql
│   └── grants_sales_dw.sql
├── migrations/
│   └── V20240601__add_customer_tier.sql
├── tests/
│   └── assert_dim_customer_unique.sql
└── snowflake.yml

其中 migrations/ 采用 Flyway 风格的版本号命名,用于记录“已经发生过的结构变更”;而 tables/、views/ 等目录保存的是“目标状态声明”。两者并存并不矛盾:前者保证变更可追溯,后者保证新环境可以从零重建。实际执行时,CI 流水线先跑 migrations 保证增量一致,再用声明式脚本做幂等校验。

二、分支模型:主干保护 + 短生命周期特性分支

对于数据平台团队,推荐采用“主干开发 + 短分支 + 发布标签”的轻量模型,而不是完整的 Git Flow。原因在于数据对象的变更往往需要与上游数据同步上线,长期存在的 develop 分支会积累大量冲突。

  • main:受保护分支,任何合并必须经过 PR 和至少一名 reviewer 批准,且 CI 全绿。main 始终对应“可部署到生产的目标状态”。
  • feature/<ticket>-<desc>:从 main 切出,生命周期不超过 3 天。例如 feature/DW-142-add-customer-tier。
  • release/<yyyy.mm.dd>:仅在需要冻结发布窗口时创建,用于回归验证,验证通过后打 tag 并合并回 main。
  • hotfix/<ticket>:从最近的生产 tag 切出,修复后同时合并回 main 和当前 release 分支。

分支保护规则应至少包含:禁止直接 push main、要求 PR 模板填写变更对象与回滚方案、要求 CI 中的 schema 校验和权限校验通过。对于 Snowflake 对象,PR 中必须附带“影响对象清单”,例如本次修改涉及 SALES_DW.MART.DIM_CUSTOMER 和 V_ORDER_SUMMARY,便于 reviewer 判断下游影响。

三、提交规范:让变更可读、可检索、可回滚

提交信息建议采用 Conventional Commits 的简化版,格式为:<type>(<scope>): <subject>。type 取值限定为 feat、fix、refactor、chore、docs、test;scope 使用 Snowflake 对象路径的末级名称,如 dim_customer、wh_etl。

feat(dim_customer): add customer_tier column
fix(v_order_summary): correct join key from cust_id to customer_id
chore(grants_sales_dw): grant select on mart to role_analyst

这样做的直接收益是:git log --oneline 就能看出每个对象最近发生了什么;配合 tag(如 v2024.06.01),可以快速定位“某次生产事故对应哪次提交”。对于 Snowflake 对象,还应在提交正文中写明变更类型:是新增列、修改数据类型、还是替换视图定义。因为不同类型的变更,回滚成本差异极大——新增列通常可逆,而删除列或修改数据类型往往需要数据迁移。

四、Snowflake 对象版本化:用迁移脚本承载“不可逆变更”

Snowflake 支持 CREATE OR REPLACE,但它对表并不友好:CREATE OR REPLACE TABLE 会先删除再重建,导致数据丢失。因此,表结构变更必须走 ALTER TABLE 迁移脚本,而视图、存储过程、任务等无状态对象可以用声明式脚本覆盖。

迁移脚本的命名与内容示例如下:

-- V20240601__add_customer_tier.sql
ALTER TABLE SALES_DW.MART.DIM_CUSTOMER
  ADD COLUMN IF NOT EXISTS customer_tier VARCHAR(20);

UPDATE SALES_DW.MART.DIM_CUSTOMER
SET customer_tier = 'STANDARD'
WHERE customer_tier IS NULL;

执行顺序由版本号决定,且每个脚本只执行一次,执行记录写入 SCHEMA_VERSION 表。这样,任何环境都能通过“重放迁移 + 声明式校验”达到一致状态。对于需要回滚的场景,应同时准备对应的 undo 脚本,例如 U20240601__drop_customer_tier.sql,并在 PR 中说明回滚触发条件。

建议在 CI 中增加一步“迁移脚本干跑”:在临时 schema 中按顺序执行所有 migrations,再与目标状态声明做 diff,确保没有遗漏或冲突。
Git 分支模型与 Snowflake 对象版本化映射 Git 仓库(版本控制层) main v2024.05.20 v2024.06.01 feature/DW-142-add-customer-tier hotfix/DW-155-fix-join 提交规范(Conventional Commits) feat(dim_customer): add customer_tier fix(v_order_summary): correct join key chore(grants): grant select to analyst type(scope): subject → 可检索、可回滚 CI/CD 迁移+校验 Snowflake(目标状态层) migrations/(增量、不可逆变更) V20240601__add_customer_tier.sql U20240601__drop_customer_tier.sql 声明式对象(可覆盖) views/v_order_summary.sql streams_tasks/task_load_orders.sql warehouses/wh_etl.sql SCHEMA_VERSION(执行记录) version | script | applied_at | status V20240601 | add_customer_tier | ... | SUCCESS main 始终对应可部署目标状态;迁移脚本保证增量一致,声明式脚本保证新环境可重建

五、把版本控制落到日常操作

一个可执行的日常节奏是:接到需求后从 main 切出 feature 分支,在 migrations/ 下新增版本脚本,同时更新对应的声明式对象文件;本地用 snowsql -f 或 CI 的临时 schema 验证脚本可执行;提交时按规范写清 type 和 scope;PR 中附上影响对象清单和回滚脚本;合并后由流水线按版本号顺序执行迁移,并写入 SCHEMA_VERSION。发布时在 main 上打 tag,tag 名与发布窗口一致,便于事后审计。

需要特别注意的是,Snowflake 对象的“版本化”并不等于“每个对象都有历史版本”。真正被版本化的是变更脚本和声明文件,Snowflake 侧只保留当前状态。因此,任何绕过 Git 直接在 Snowflake 控制台执行的 DDL,都会造成仓库与实例漂移。建议在 CI 中增加漂移检测:定期对比 INFORMATION_SCHEMA 中的对象定义与仓库声明文件,发现差异即告警。这样,版本控制才真正成为 DataOps 的可信基线。