dbt+SQLServer构建数据仓库(7):进阶扩展
dbt+SQLServer构建数据仓库(7):进阶扩展
本文讲述项目从"能跑"走向"生产级"的四个进阶能力:增量模型只跑新增数据、快照做 SCD2 历史拉链、dbt docs 自动生成文档与血缘、CI/CD 让每次提交自动验证。这些功能本项目暂未实现,本文给出可直接落地的代码示例。
一、引言
前 6 篇我们走完了从认知到建模到测试的完整闭环,但"能跑"离"生产级"还有四个能力缺口:
| 能力 | 解决的问题 | 本文章节 |
|---|---|---|
| 增量模型 | 大数据量全量重建太慢 | 第二节 |
| 快照(snapshot) | 历史状态丢失,无法追溯 | 第三节 |
| dbt docs | 文档靠口口相传,新人难上手 | 第四节 |
| CI/CD | 提交即验证,质量门禁缺失 | 第五节 |
本文给出每个能力的可落地代码示例,可直接加到本项目的 dbtms/ 目录里。需要说明:这些功能本项目暂未实现,目的是把"如何落地"讲清楚,供你按需引入。
二、增量模型(incremental)
2.1 问题:全量重建的代价
本项目 [fct_orders.sql]现在用 table 物化(见 [dbt_project.yml] 第 33 行 marts: +materialized: table)。每次 dbt run 都会把整张表 drop 再 create,全量重算。
数据量小(几千几万行)没问题,一旦源表到了百万行、千万行,每次跑几分钟甚至几十分钟,日跑批就扛不住了。
2.2 解决:改用 incremental 物化
incremental 物化的核心思想:首次全量构建,后续只处理新增数据。改造 [fct_orders.sql]
{{ config(materialized='incremental',unique_key='order_id',on_schema_change='append_new_columns'
) }}with orders as (select * from {{ ref('stg_orders') }}{% if is_incremental() %}where order_date > (select max(order_date) from {{ this }}){% endif %}
),payments as (selectorder_id,sum(amount) as total_amountfrom {{ ref('stg_payments') }}where status = 'completed'group by order_id
)selecto.order_id,o.customer_id,o.order_date,o.status,coalesce(p.total_amount, 0) as amount
from orders o
left join payments pon o.order_id = p.order_id
2.3 三个关键配置
| 配置 | 作用 |
|---|---|
materialized='incremental' |
启用增量物化,模型从 table 变为增量表 |
unique_key='order_id' |
去重键。重复跑同一天的数据不会产生重复行(SQL Server 上走 MERGE INTO) |
is_incremental() |
Jinja 宏。首次构建返回 false(走全量),后续返回 true(走 where 过滤) |
{% if is_incremental() %} 块里的 where order_date > (select max(order_date) from {{ this }}) 是增量的灵魂:{{ this }} 指向当前模型已存在的表,只挑出比已有最大日期更新的数据。
2.4 增量策略(SQL Server)
dbt-sqlserver 支持两种增量策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
append(默认) |
直接 INSERT 新数据 | 源数据只新增、绝不修改历史 |
merge |
MERGE INTO,基于 unique_key 去重 upsert |
源数据可能更新已有行 |
如需用 merge,在 config 里加 incremental_strategy='merge':
{{ config(materialized='incremental',unique_key='order_id',incremental_strategy='merge',on_schema_change='append_new_columns'
) }}
三、快照(snapshot/SCD2)
3.1 问题:历史状态丢失
[fct_orders.sql] 只反映当前状态。客户改了名字、订单从 pending 变成 completed,旧状态就丢了——而审计、对账往往要问"上周三这个订单是什么状态"。
3.2 解决:snapshot 做 SCD2
snapshot 自动实现 Type 2 Slowly Changing Dimension(SCD2):每次源数据变化,就追加一行新版本,旧行打上失效时间,形成"历史拉链表"。
在项目根目录新建 snapshots/snap_orders.sql:
{% snapshot snap_orders %}
{{config(target_schema='dbt_dev_snapshots',unique_key='order_id',strategy='timestamp',updated_at='order_date',)
}}
select * from {{ source('raw', 'raw_orders') }}
{% endsnapshot %}
3.3 四个关键配置
| 配置 | 作用 |
|---|---|
target_schema |
快照表存放的 schema,本项目约定 dbt_dev_snapshots |
unique_key |
主键,用于识别"同一行"在不同时点的版本 |
strategy='timestamp' |
基于时间戳判断是否变化(另一种是 check,比对指定字段) |
updated_at='order_date' |
时间戳字段,dbt 用它判断该行是否比上次快照更新 |
3.4 执行与产出
dbt snapshot
dbt 会自动给快照表多加几个元数据列:
| 列名 | 含义 |
|---|---|
dbt_scd_id |
每个版本行的唯一标识(哈希) |
dbt_updated_at |
本次变更发生的时间 |
dbt_valid_from |
本版本生效起始时间 |
dbt_valid_to |
本版本失效时间(当前版本为 NULL) |
查询"某订单的历史状态流转":
select order_id, status, dbt_valid_from, dbt_valid_to
from dbt_dev_snapshots.snap_orders
where order_id = 1001
order by dbt_valid_from;
3.5 使用场景
- 客户改名历史:追溯任意时点的客户名称
- 订单状态流转:pending → paid → shipped → completed 的时间线
- 审计追溯:合规检查需要"某天某时刻的状态"
四、dbt docs:自动生成文档与血缘
4.1 两条命令
dbt docs generate # 生成文档(写入 target/ 目录)
dbt docs serve # 启动本地网站(默认 8080 端口)
4.2 产出一个可点击的网站
打开 http://localhost:8080,你会看到:
- 模型列表与描述:所有 model 一目了然
- 每个模型的 SQL、编译后 SQL、字段说明:点开模型即可看
- DAG 血缘图:可视化展示模型依赖,节点可点击钻取
- 源表(source)描述:raw 层来源说明
4.3 文档来源:schema.yml
文档不是手写的,而是从 schema.yml 里的 description 和 columns 自动抽取。本项目已有的 schema.yml 越完整,生成的 docs 就越丰富。例如:
models:- name: fct_ordersdescription: 订单事实表,每个订单一行,关联客户与已完成支付的金额columns:- name: order_iddescription: 订单主键tests: [unique, not_null]- name: customer_iddescription: 关联 dim_customers 的外键- name: amountdescription: 已完成支付的金额总额,未支付为 0
4.4 优势与团队协作价值
- 文档从代码生成:永远和代码同步,不像 Word 文档会腐烂
- 新人友好:看 docs 网站就能理解数仓结构,不用读代码
- 血缘可视:依赖关系一眼看清,改上游能立刻看到下游影响
五、CI/CD:每次提交自动验证
5.1 GitHub Actions 示例
在项目根目录新建 .github/workflows/dbt_ci.yml:
name: dbt CI
on: [pull_request]
jobs:dbt:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: actions/setup-python@v5with: { python-version: '3.11' }- run: pip install dbt-core dbt-sqlserver- run: dbt deps- run: dbt parse- run: dbt build --target cienv:DBT_SQLSERVER_HOST: ${{ secrets.CI_DB_HOST }}DBT_SQLSERVER_USER: ${{ secrets.CI_DB_USER }}DBT_SQLSERVER_PASSWORD: ${{ secrets.CI_DB_PASSWORD }}
5.2 CI 流程
PR 提交 → dbt parse(语法检查) → dbt build(run + test) → 全过才能 merge。
dbt parse:只解析不执行,秒级反馈语法错误dbt build:第 5 篇讲过,一次跑完 run + test,任一失败 CI 红
5.3 环境隔离
CI 用独立的 ci target(在 profiles.yml 里配 schema: dbt_ci_),不污染 dev/prod。每个 PR 跑在自己的 schema 前缀下,互不干扰。
5.4 调度生产化
CI 解决的是"提交时验证",生产调度是另一回事。两种方案:
| 方案 | 做法 | 适合 |
|---|---|---|
| 方案 A | SQL Server Agent 调 dbt run --target prod |
已有 Agent 体系,复用现有调度 |
| 方案 B | Airflow/Dagster 编排(cosmos 或 dbt operator) | 需要复杂依赖、重试、告警 |
方案 A 简单:在 Agent Job 里加一步 cmdexec,调用 dbt run --target prod。方案 B 更强大:能可视化 DAG、失败重试、上下游联动告警,但引入了新的编排系统。
六、生产化检查清单
从开发到生产,逐项对照:
| 维度 | 检查项 | 本系列参考篇 |
|---|---|---|
| 配置 | dbt_project.yml flags 配齐 | 第3篇 |
| 建模 | 分层清晰,staging 1:1,marts 聚合 | 第4篇 |
| 测试 | 主键 unique+not_null,外键 relationships | 第5篇 |
| 模板 | ref/source 正确,无硬编码表名 | 第6篇 |
| 增量 | 大表用 incremental,有 unique_key | 本文第二节 |
| 快照 | 需要历史追踪的表配 snapshot | 本文第三节 |
| 文档 | dbt docs 可生成,schema.yml 描述完整 | 本文第四节 |
| CI | PR 自动跑 dbt build | 本文第五节 |
| 调度 | Agent/Airflow 定时调 dbt run --target prod | 本文第五节 |
| 监控 | run_results.json 解析,测试失败告警 | 第1篇 |
清单不是死规定,按团队实际情况裁剪。核心是:配置、建模、测试、模板四项是底线,增量/快照/CI 按需引入。
七、下一步
下一步将借助AI的能力来进一步扩展dbt的能力,我们会搭建一套vibe coding的环境,来自动构建数据仓库。
八、小结
本文讲了项目从"能跑"走向"生产级"的四个进阶能力:
- 增量模型:大数据量只跑新增,
is_incremental()+unique_key是核心 - 快照:SCD2 历史拉链,
strategy='timestamp'+updated_at是核心 - dbt docs:文档从代码生成,
dbt docs generate+serve两条命令搞定 - CI/CD:GitHub Actions 跑
dbt build --target ci,PR 即门禁
这四个能力的共同点:都是 dbt 原生支持,不用额外买工具。增量靠 config,快照靠 snapshot 资源类型,文档靠 docs 命令,CI 靠标准 YAML——这正是 dbt 作为"工程化框架"的价值。
---------------------------------------------------------------
来自博客园的aspnetx宋卫东
