主题: 第10部分. dbt 的 Staging 模型
难度等级: 中级
预计学习时间: 2.5–3 小时(理论 ~1 小时,实操 ~1.5 小时,案例分析 ~30 分钟)
前置要求: 熟练掌握 SQL(SELECT、JOIN、CAST、COALESCE、CASE)
对 dbt 架构有基本理解(sources、models、ref、tests)
熟悉 ELT 原则与数据仓库分层架构(raw → staging → intermediate → marts)
了解 Specification-Driven Development(SDD,规格驱动开发)与 Schema Manifest 的概念
具备 dbt 项目的最低实操经验(dbt run、dbt test)
学习目标: 区分 dbt staging 层中允许与禁止的操作,并能论证技术规范化与业务逻辑之间的边界。
结合源系统的事实与规格说明,正确应用类型转换、空值处理与 null 语义。
为 staging 模型设计 dbt 测试(not_null、unique、accepted_values、singular),并将它们与 mart 层的验证事实区分开来。
识别 staging 中隐藏的业务逻辑,并提出将其迁移至规格或 mart 的要求。
以 SDD 风格记录 staging 中关于 PII、主键与契约漂移的决策。
概述: dbt 中的 staging 模型是第一层,将来自源的原始数据转变为具有明确类型、一致命名与显式 null 语义处理的稳定技术模型。本章阐述了 staging 的设计哲学:「此处的平淡是有益的」。Staging 不应回答产品层面的问题,不应选择 mart 的 grain,不应对客户进行聚合,也不应隐藏源契约的漂移。其任务是准备数据,使下游 SQL 可预测,并使评审者能快速将技术规范化与产品语义区分开来。本节将讨论允许与禁止操作的边界、类型转换的模式(包括在 CAST 为 date 之前处理空字符串)、基础的 dbt 测试集、PII 处理要求、典型错误(例如「在 staging 中隐藏业务逻辑」),以及帮助代理以 SDD 精神处理 staging 的复习问题与 Qwen 提示词。
关键概念: Staging 作为技术层:Staging 是数据转换的第一层,数据在此「开始说平台语言」。在这一层会出现稳定的列名、显式的类型、空值处理与基础主键,但不会做出任何产品决策。如果 staging 模型变成了「小型业务 mart」,那么这一层就变得过于聪明了。
允许操作的边界:在 staging 中允许的操作包括:将字段名规范化为项目约定、类型转换(date、integer、decimal)、显式处理空值与 null 语义、保留源主键、标记 PII 字段(但在没有正当理由时不进行隐藏)。这些操作属于技术层面,不回答消费者的问题。
禁止操作的边界:在 staging 中禁止的操作包括:对客户进行聚合、计算风险评分、选择 mart 的最终 grain、为「美观」删除行、隐藏源契约的漂移。任何此类逻辑必须明确写入规格/manifest 中,或迁移至 intermediate/mart 层。
类型转换与 null 语义:典型模式为:try_cast(nullif(cast(revoked_at as varchar), '') as date) as revoked_at。该行将空值转换为 null,但「revoked_at 为 null = 同意仍然有效」的语义被记录在规格中并在 mart 中加以考虑。技术转换始终伴随产品层面的注释。
Staging 中的 dbt 测试:最小测试集包括:源主键的 not_null 与 unique;必需金额与日期的 not_null;值域较小时的 accepted_values;以及用于复杂约束的 singular 测试。Staging 测试捕获源的技术问题,但不能替代数据产品的验证。
Sdd 与决策记录:Specification-Driven Development 要求:记录空值在 raw 中出现的情况;解释为何在 staging 中它变为 null;验证 mart 是否一致地计算了有效与已撤销的同意。技术修正被转化为有据可查的决策。
Staging 中的 Pii:PII 字段仅在需要展示安全策略的位置才在 staging 中保持可见。「默认」隐藏它们是一种反模式:这会掩盖契约漂移并妨碍验证。
Staging 的 Qwen 提示词:代理的标准提示词为:「阅读 Schema Manifest 与 raw 源。创建或检查 staging 模型。不要添加业务聚合。对于每个类型转换,请解释其所基于的源事实。」这种提示词明确了边界,并降低了「智能」模型的风险。
面向代理的 SQL 风格:Staging 模型应当精简、自上而下易于阅读,并在适当位置使用 ref() 与 source()。如果 staging 难以阅读,下游模型只会更糟糕。
典型错误:staging 中的风险策略:在 staging 中隐藏业务逻辑(例如:case when amount_rub > 100000 then 1 else 0 end as risk_event)仅在该规则已记录在规格或源 manifest 中时才允许。否则,代理就是在凭空创造风险策略,这违反了 SDD。
重要日期: 2020:dbt Labs 正式在文档中确立了 staging → intermediate → marts 的层级术语,从而简化了架构标准化。
2021:dbt-utils 发布并推广了 tests/generic_tests 宏,简化了 accepted_values 与 singular 测试的编写。
2022:dbt-meta-testing 与契约式测试的出现,强化了对 staging 层源 schema 漂移的控制。
2023:dbt Contracts(模型版本)的引入——staging 成为防御源契约漂移的第一道防线。
2024:SDD 实践与 Schema Manifest 作为 AI 代理生成 staging 模型之前的强制产物的广泛普及。
练习题: 名称: 审计 staging:区分技术与产品
问题: 给定 staging 模型 stg_consents.sql。模型主体:
select
user_id,
cast(consented_at as date) as consented_at,
try_cast(nullif(cast(revoked_at as varchar), '') as date) as revoked_at,
case when revoked_at is null then 'active' else 'revoked' end as status,
case when amount_rub > 100000 then 1 else 0 end as risk_event,
count(*) over (partition by user_id) as consents_per_user
from {{ source('raw', 'consents') }}
任务:
- 为每个转换打上标签:重命名、类型转换、null 处理、业务逻辑。
- 指出每行所依据的源事实。
- 找出那些没有显式规格说明的业务逻辑。
- 重写该模型,使 staging 中仅保留技术转换,而产品逻辑被迁出或标记为需要规格说明。
解决方案: 步骤 1 —— 为每个转换打上标签:
- cast(consented_at as date) —— 类型转换(技术性,事实:raw 中该字段为字符串,值域为日期)。
- try_cast(nullif(cast(revoked_at as varchar), '') as date) —— null/空字符串处理(技术性,事实:raw 中存在会破坏向 date 的 cast 的空值)。
- case when revoked_at is null then 'active' else 'revoked' end as status —— 业务逻辑。「active」语义属于规格,而非 staging。应当迁至 mart 或记录在 Schema Manifest 中。
- case when amount_rub > 100000 then 1 else 0 end as risk_event —— 业务逻辑。100,000 的阈值规则属于风险策略,不应出现在 staging 中。将其隐藏于此是典型错误。
- count(*) over (partition by user_id) as consents_per_user —— 聚合。在 staging 中禁止。
- user_id —— 保留源主键(技术性,事实:raw 中存在该主键,应予以保留)。
步骤 2 —— staging 的正确版本:
select
user_id,
cast(consented_at as date) as consented_at,
try_cast(nullif(cast(revoked_at as varchar), '') as date) as revoked_at,
amount_rub
from {{ source('raw', 'consents') }}
步骤 3 —— 在模型或 Schema Manifest 中添加注释:
- revoked_at:「raw 中的空值 → staging 中的 null;active/revoked 语义根据规格 S-001 在 mart 中定义」。
- amount_rub:「字段按原样保留,业务规则(例如 risk_event)根据规格在 mart 中计算」。
结论:模型变得「平淡」且可预测,业务逻辑被迁移至其应在的位置。
难度: intermediate
名称: 针对同意表的 staging dbt 测试集
问题: 源为表 raw.consents,包含字段:user_id (string)、consented_at (string,格式为 YYYY-MM-DD)、revoked_at (string,可能为空)、source_system (string,值域:'web'、'mobile'、'partner_api')。请为 stg_consents 创建一个包含测试的 schema.yml 文件,并论证为何这些测试属于技术层面而非产品层面。
解决方案: ```yaml version: 2
models:
- name: stg_consents
description: "对 raw.consents 中的同意数据进行技术规范化。active/revoked 语义根据规格 S-001 在 mart 中定义。" columns:
- name: user_id
description: "来自源系统的用户标识符。staging 模型的主键。" tests:
- not_null
- unique
- name: consented_at
description: "同意授予日期。源:字符串字段,已在 staging 中转换为 date。" tests:
- not_null
- name: revoked_at
description: "同意撤销日期。raw 中的空值 → staging 中的 null。" tests:
- not_null
# 替代方案:仅当 revoked_at 按契约始终填充时,not_null 才合适; # 否则应替换为 singular 测试,验证空值已转换为 null。
- name: source_system
description: "同意授予的来源渠道。" tests:
- accepted_values:
values: ['web', 'mobile', 'partner_api']
测试技术性的论证:
- user_id 上的 not_null + unique 检查的是源主键的完整性,而非产品假设。
- consented_at 上的 not_null 固定了契约,即授予日期始终存在。
- source_system 上的 accepted_values 可捕获未经过上线流程的新渠道,并提示契约漂移。
- 产品级测试(例如「不存在 revoked_at 晚于 consented_at 的用户」、「同一类型不存在两个有效同意」)应写在 mart 或 tests/ 文件夹下,使用 singular 测试进行数据产品验证。
难度: intermediate
名称: 处理 revoked_at 的空值:从源事实到文档化的决策
问题: 在 raw.consents 中,revoked_at 字段以字符串形式传入,其中 30% 的行包含空字符串。当前代码为:cast(revoked_at as date) as revoked_at。对于空字符串,dbt 会抛出错误或返回意外值。请重写该转换,并为 Schema Manifest 准备注释。
解决方案: 正确的转换:
try_cast(nullif(cast(revoked_at as varchar), '') as date) as revoked_at
逻辑:
1. cast(revoked_at as varchar) —— 规范化类型(以防源中传入其他类型)。
2. nullif(..., '') —— 将空字符串转换为 null。这是一个技术步骤,不是产品决策。
3. try_cast(... as date) —— 安全地转换为 date;若字符串无法解析,则得到 null。
Schema Manifest 中的记录(片段):
fields:
- name: revoked_at
raw_type: string raw_semantics: "同意撤销日期。raw 中存在空字符串(约 30% 的行),表示「同意未被撤销」。" staging_type: date staging_semantics: "来自 raw 的空字符串已转换为 null。「null = 同意仍然有效」的语义已在规格 S-001 中固化,并在 mart 中实现。" mart_usage: "mart.consent_status 按以下规则计算 active/revoked:revoked_at is null → active,否则 → revoked。"
结论:技术修正(try_cast + nullif)成为文档化决策的一部分,在 staging、规格与 mart 之间保持一致。
难度: intermediate
案例研究:
名称: 金融科技初创公司案例:「staging 中的风险评分破坏了监管报告」
场景: 一家金融科技初创公司的分析团队部署了一个 AI 代理,根据 Schema Manifest 生成 staging 模型。代理生成了 stg_transactions,其中除技术转换外还新增了一列 risk_event:case when amount_rub > 100000 then 1 else 0 end。100,000 的阈值规则未在规格中固定——代理是依据通用的 AML 合规实践「自行编造」的。
挑战: 三个月后,监管机构要求提交可疑交易报告。团队基于 staging 中的 risk_event 构建了 mart.suspicious_transactions 并提交给监管机构。内部审计发现:(1) 100,000 的规则在任何地方都未记录;(2) 另一团队(compliance)依据最新版本的策略并行使用了 250,000 的阈值;(3) mart 与 compliance 的监管报告存在 12% 的行级差异。团队不得不紧急审查架构并向监管机构解释规则的来源。
解决方案: 团队进行了复盘并采取了以下措施:
1. 从 stg_transactions 中删除了 risk_event,仅保留技术转换:类型转换、null 处理、字段重命名。
2. 在 Schema Manifest 中描述了 risk_event 规则,注明来源(compliance policy v2.3,阈值 250,000)以及监管文件的引用。
3. 将逻辑迁移至 mart.suspicious_transactions,使规则显式化、可版本化,并由产品负责人签字。
4. 在 mart 中添加了 dbt 测试:singular 测试,验证所有 risk_event = 1 的行都满足 amount_rub >= 250000 且 policy_version = 'v2.3'。
5. 在给代理的提示词中加入了显式限制:「不要在 staging 中添加业务聚合和阈值。如果发现规则未在 Schema Manifest 中固定,请将其标记为 TODO,不要生成该列」。
结果: 监管报告在 2 天内重新生成,与 compliance 的差异消除。团队正式确立了规则:staging 仅接受本章所列举的决策(字段名、类型、null、主键、PII 标记)。业务逻辑仅在引用规格的前提下才允许出现在 staging 中。一个季度后,类似关于 1,000,000 阈值的情况被代理在生成阶段捕获——它输出了 TODO 而非该列,团队及时更新了 Schema Manifest。
经验教训:
Staging 不是放置阈值与业务分类的场所:即使是「无害的」case when 也会引发监管风险。
Staging 中的任何数值规则都必须引用规格或源 manifest;否则就是代理凭空创造的策略。
给代理的提示词必须显式禁止业务聚合,并要求任何非标准列必须引用规格。
在 Schema Manifest 中记录 null 语义与阈值,可使审计与报告重生成的成本大幅降低。
相关概念:
Staging 中允许/禁止操作的边界
SDD 与 Schema Manifest
典型错误:staging 中的风险策略
Staging 的 Qwen 提示词
Staging 测试与 mart 验证事实的区别
名称: 电商平台案例:「在 staging 中隐藏 PII 导致调试失败」
场景: 一家电商平台的数据工程师决定「在 staging 阶段就保护数据」,并在 stg_customers 中将 PII 字段(email、phone)直接掩码为哈希值。这一操作未经安全团队批准,也未记录在 Schema Manifest 中,属于「以防万一」的做法。半年后,反欺诈分析团队无法复现一起事故:调查需要原始 email,但 staging 中只存储了哈希值,而源 raw.customers 已被轮换且无法访问。
挑战: 反欺诈团队无法完成所声称事故的调查,监管机构(在反欺诈报告层面)要求确认特定 email 确实出现在可疑交易中。在无法重新从源系统导出(需要额外审批且耗时 5 个工作日)的情况下,「email → 用户 → 交易」的关联关系无法重建。
解决方案: 团队进行了审计并固化了新规则:
1. PII 在 staging 中默认保持可见;仅在 mart 层为特定消费者(例如营销 mart)执行掩码。
2. 在 Schema Manifest 中,每个 PII 字段都注明敏感级别(low/medium/high)以及安全策略的引用。
3. stg_customers 中的 email 与 phone 列被恢复为原始形式,掩码则在 mart.marketing_customers 中通过应用 mask_pii() 宏的 view 实现。
4. 为反欺诈团队创建了独立的 mart.fraud_investigation,其中 PII 以明文形式保留,访问通过 warehouse 层的 grant 控制,而非通过 staging 中的转换来控制。
结果: 事故调查在变更上线后 1 天内完成。安全团队获得了 Schema Manifest 中明确的 PII 字段清单,并能够按层级(而非凭工程师「直觉」)建立访问策略。一个季度后,类似关于电话的事件被阻止:一名初级分析师请求代理「在 staging 中掩码 phone」——代理引用策略拒绝了该请求,并建议创建独立的 mart。
经验教训:
Staging 中的 PII 应保持可见:掩码是产品层面的决策,属于 mart 层。
在 staging 中隐藏 PII 会掩盖源契约漂移:如果字段消失或格式改变,你将过晚地发现。
PII 的访问策略应在 grant 与角色层面进行管控,而非通过 staging 中的转换。
Schema Manifest 是记录源中存在哪些 PII 字段以及在哪里进行掩码的强制场所。
相关概念:
Staging 中的 PII
Schema Manifest
允许操作的边界(标记但不隐藏)
在 SDD 中记录决策
学习建议:
在编写 staging 模型之前,请打开 Schema Manifest 并高亮没有规格说明的字段。如果超过两个,请停下来向产品负责人索取规格。
「平淡 staging」原则:如果模型自上而下易于阅读,没有分支与 CASE,那么很可能位于正确的层。如果你发现自己在想「这里需要注释来解释逻辑」,那么该逻辑不属于 staging。
对于 raw 中可能为空值的字段,始终应用 try_cast(nullif(cast(... as varchar), '') as date) 模式。这可以消除 90% 的类型转换错误。
在提交 staging 模型之前,逐行打上标签:「重命名」、「类型转换」、「null 处理」、「业务逻辑」。如果最后一类没有引用规格,请删除该行。
对于 Qwen/AI 代理,请使用本章中的标准提示词作为模板,并加入显式禁止:「不要添加带阈值的 case when」、「除技术性 row_number 外不要使用窗口函数」。
Staging 中的 dbt 测试应当是技术性的:not_null、unique、accepted_values。产品级测试(例如「同一类型不存在两个有效同意」)位于 mart。
如果需要隐藏 PII,请创建独立的 mart,而非修改 staging。Staging 是契约漂移应被看到的位置。
定期(每季度)对 staging 进行审计:寻找那些没有引用规格的业务逻辑行。这是防止该层「变聪明」的最可靠方式。
对比 staging 与 mart 时遵循以下原则:「staging 回答『源中传来了什么』,而 mart 回答『这对业务意味着什么』」。如果 staging 开始回答第二个问题,则它不属于这一层。
附加资源:
Dbt 文档 — staging 模型:https://docs.getdbt.com/best-practices/how-we-structure/1-staging —— dbt Labs 关于 staging 层的权威描述,包含示例与反模式。
Dbt-utils(测试与宏):https://github.com/dbt-labs/dbt-utils —— 提供 not_null、unique、accepted_values、expression_is_true 与 singular 测试的宏集合。
Dbt contracts(模型版本控制):https://docs.getdbt.com/docs/collaborate/govern/model-contracts —— 模型合约文档,强化 staging 中的漂移防护。
《analytics engineering with dbt》一书:https://www.amazon.com/Analytics-Engineering-dbt-Airflow-BigQuery/dp/1098142370 —— 涉及分层与 staging/marts 边界的章节。
How we structure our dbt projects(discourse):https://discourse.getdbt.com/t/how-we-structure-our-dbt-projects/355 —— dbt Labs 团队关于层级结构的经典文章。
Specification-driven development (sdd):https://martinfowler.com/articles/replaceThrowWithException.html —— 在 dbt 社区文章中被适配到分析领域的 SDD 通用原则。
Qwen 文档(提示工程):https://qwen.readthedocs.io/en/latest/ —— Qwen 文档,用于为分析代理编写高效的提示词。
Dbt-project-evaluator:https://github.com/dbt-labs/dbt-project-evaluator —— 自动化的项目结构测试,可捕获带有意外逻辑的 staging 模型。
摘要: dbt 中的 staging 模型是技术层,它将 raw 数据转变为稳定的、类型化的、有据可查的模型,而不做出产品决策。Staging 中允许的操作包括:字段名与类型的转换、显式处理 null 与空值、保留源主键、标记 PII。Staging 中禁止的操作包括:聚合、业务过滤、选择 mart grain、计算风险评分、为「美观」删除行、隐藏契约漂移。关键模式 —— try_cast(nullif(cast(... as varchar), '') as date) —— 安全地将空字符串转换为 null,而「null = 同意仍然有效」的语义被记录在规格中并在 mart 中实现。Staging 中的 dbt 测试属于技术性质(not_null、unique、accepted_values,以及用于复杂约束的 singular),它们捕获源问题,但不能替代数据产品的验证。主要陷阱是在 staging 中隐藏业务逻辑:case when amount_rub > 100000 then 1 else 0 end as risk_event 仅在引用规格时才允许,否则就是代理凭空创造的策略。Staging 中的 PII 保持可见;掩码属于 mart 的任务。优秀的 staging 是「平淡的」:精简、自上而下可读、使用 ref()/source(),且易于评审。如果 staging 变得复杂,下游模型只会更糟糕;如果 staging 易于理解,评审者就能快速将技术规范化与产品语义区分开来。完成本章后,你应该能够:为主要源创建 staging 模型、使用 dbt 测试覆盖主键、将 null 相关的决策记录在 Schema Manifest 中,以及在需要展示策略的位置保持 PII 可见。