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 中说明回滚触发条件。
五、把版本控制落到日常操作
一个可执行的日常节奏是:接到需求后从 main 切出 feature 分支,在 migrations/ 下新增版本脚本,同时更新对应的声明式对象文件;本地用 snowsql -f 或 CI 的临时 schema 验证脚本可执行;提交时按规范写清 type 和 scope;PR 中附上影响对象清单和回滚脚本;合并后由流水线按版本号顺序执行迁移,并写入 SCHEMA_VERSION。发布时在 main 上打 tag,tag 名与发布窗口一致,便于事后审计。
需要特别注意的是,Snowflake 对象的“版本化”并不等于“每个对象都有历史版本”。真正被版本化的是变更脚本和声明文件,Snowflake 侧只保留当前状态。因此,任何绕过 Git 直接在 Snowflake 控制台执行的 DDL,都会造成仓库与实例漂移。建议在 CI 中增加漂移检测:定期对比 INFORMATION_SCHEMA 中的对象定义与仓库声明文件,发现差异即告警。这样,版本控制才真正成为 DataOps 的可信基线。