学习指南: 第 11 部分. 数据验证:validation.md

模块「第 11 部分. 数据验证:validation.md」中第 3 / 5 节课
您正在未登录状态下查看课程。 请登录,以保存进度并参加测试。

主题: 第 11 部分。数据验证:validation.md

难度等级: 中等

预计学习时间: 2-3 小时

前置要求: 对 dbt 的基本理解(模型、测试、schema.yml)

具备编写 SQL 查询和 singular 测试的能力

熟悉 ODCS 和 ODPS 规范

理解 Specification-Driven Development (SDD) 的概念

具备命令行操作和运行 dbt 命令的经验

学习目标: 将验证事实表述为命令、SQL 查询或评审员的手动步骤,并将其与愿望区分开来

将事实划分为自动和手动两类,并为每个选择说明理由

为数据集市创建一个最小的 validation.md,包括有关 grain、PII、契约、lineage 和手动评审的事实

在编写 SQL 代码之前应用 validation.md 编写实践,以限制解决方案的规模

将多个验证层(规范、ODCS、schema.yml、dbt 测试)链接到统一的验证工件中

概述: 在 Specification-Driven Development 中,数据验证不是一个抽象的断言“数据是正确的”,而是一组可以执行的具体事实。每个事实必须将模型规范、SQL 工件和证明联系起来。如果一个事实不能通过命令执行、SQL 查询或评审员的手动步骤来确认,那么它仍然只是一个愿望。validation.md 文档以可版本控制和可读的形式记录这些事实,将数据发布从一堆 SQL 补丁转变为工程过程。本指南讨论 validation.md 的结构、最小事实集、自动和手动验证之间的区别,以及在编写代码之前进行验证的实践。

关键概念: 验证事实 (Проверочный факт):关于数据的断言,可以通过以下三种方式之一执行:命令(例如 dbt test)、SQL/singular 测试或评审员记录在案的手动步骤。事实将规范、SQL 和证明联系起来。没有这种联系,断言仍然是愿望。

自动事实 (Автоматический факт):通过命令运行并以退出代码或行集结束的验证事实。示例:dbt test --select mart_customer_360 预期退出代码为 0 且无错误。适用于可在架构和数据逻辑中形式化的属性:唯一性、not_null、范围、格式。

手动事实 (Ручной факт):需要阅读变更或产品负责人/评审员确认的验证事实。在解决方案依赖于契约上下文的情况下是必需的:例如,向数据集市的 grain 中添加新字段是否可接受。手动事实只有在明确指出要阅读什么以及应得出什么结论时才具有说服力。

数据集市的 grain (Зерно (grain) витрины):定义在数据集中什么算作一行(例如,一个 customer_id = 一行)。关于 grain 的事实通常是自动的:检查键的唯一性和 not_null。Grain 的更改需要手动评审。

PII 防护 (Защита pii):验证直接的个人数据(电子邮件、电话、护照)未进入数据集市。通过 singular 测试实现,当模型的输出列中存在 PII 字段时返回 0 行。

数据集市契约 (Контракт витрины):必需字段、其类型和规则的列表,记录在 schema.yml 和 ODCS 中。契约的验证事实是对必需性和类型的测试,加上一个手动事实:变更不会在没有明确断言的情况下更改契约字段。

Lineage 和输入模型 (Lineage и входные модели):数据集市从中收集的源列表。验证事实记录了哪些模型作为输入以及源是否在未更新规范的情况下被更改。

Sdd (specification-driven development):一种开发方法,其中规范在代码之前编写。validation.md 是 SDD 的工件之一,它使规范可验证并能抵御变更。

Dbt singular 测试 (Singular-тест dbt):tests/assert_*.sql 文件中的 SQL 查询,当规则被违反时返回行。空结果 = 测试通过。用于无法用 schema.yml 的 generic 测试表达的复杂检查。

弱事实与强事实 (Слабый факт vs сильный факт):弱事实是诸如“检查数据质量”或“看起来正常”之类的短语。强事实包含命令、预期结果和状态:“dbt test --select mart_customer_360 返回代码 0,customer_id 唯一且 not_null,状态:已接受”。

重要日期: Sdd 中 validation.md 的概念:在编写 dbt 模型之前的数据项目(mart)设计阶段出现。文件存储在 specs/validation/ 中,并随规范和代码一起在 Git 中进行版本控制。

事实重新审视的时刻:当任何层发生更改时:模型规范、ODCS 契约、models/schema.yml、dbt 模型或源组成。validation.md 必须明确显示哪些事实需要重新审视。

练习题: 名称: 将弱点重写为三种形式

问题: 在旧版本发布检查表中,有一项“检查 mart_customer_360 中的数据质量”。将其重写为三种形式:1) 带有预期结果的 dbt 命令,2) 带有应返回内容描述的 SQL/singular 测试,3) 评审员的手动事实,指定要阅读什么以及应得出什么结论。

解决方案: 形式 1(dbt 命令):

### F1 — 键质量
- 命令: dbt test --profiles-dir . --select mart_customer_360
- 预期: 退出代码为 0;customer_id 的 unique 和 not_null 测试通过。
- 状态: 已接受。

形式 2(singular 测试):

-- tests/assert_customer_360_no_orphan_orders.sql
select c.customer_id, c.last_order_id
from {{ ref('mart_customer_360') }} c
left join {{ ref('dim_orders') }} o on o.order_id = c.last_order_id
where c.last_order_id is not null and o.order_id is null
- 命令: dbt test --profiles-dir . --select assert_customer_360_no_orphan_orders
- 预期: 测试返回 0 行。
- 状态: 已接受。

形式 3(手动事实):

### F3 — grain 评审
评审员确认向 mart_customer_360 添加 product_code 会改变 grain(每个 customer × product 一行,而不是 customer),并且不包括在当前阶段。决定已记录在工单 DATA-1234 中。

难度: beginner

名称: 为数据集市构建最小的 validation.md

问题: 对于数据集市 mart_orders_summary(一行 = 一个 order_id),创建一个最小的 validation.md,其中包括:有关 grain 的事实、有关 PII 的事实、有关契约必需字段的事实、有关 lineage/输入模型的事实以及一个手动事实。模型规范:models/schema.yml 包含 order_id (unique, not_null)、customer_id (not_null)、total_amount (not_null)、order_date (not_null)。源:stg_orders、stg_customers。PII:email、phone — 不应进入数据集市。

解决方案: ```markdown

validation.md — mart_orders_summary

F1 — 数据集市的 grain

  • 命令: dbt test --profiles-dir . --select mart_orders_summary
  • 预期: order_id 唯一且 not_null,退出代码为 0。
  • 状态: 已接受。

F2 — PII 未进入数据集市

  • 命令: dbt test --profiles-dir . --select assert_orders_summary_no_pii
  • 预期: 测试返回 0 行(输出列中没有 email/phone 字段)。
  • 状态: 已接受。

F3 — 契约的必需字段

  • 命令: dbt test --profiles-dir . --select mart_orders_summary
  • 预期: customer_id、total_amount、order_date 全部 not_null。
  • 状态: 已接受。

F4 — Lineage 和输入模型

  • 命令: dbt list --select +mart_orders_summary --output name
  • 预期: 列表包含 stg_orders 和 stg_customers 作为直接前置项。
  • 状态: 已接受。

F5 — grain 变更评审

评审员确认向 mart_orders_summary 添加 order_line_id 会改变 grain,并且不包括在 MVP 阶段。决定已记录在工单 DATA-7788 中。

难度: intermediate

名称: 查找并修复典型错误

问题: 在项目中,一位同事在 validation.md 中写了一个事实:“数据集中的数据正确且已准备好发布”。指出为什么这是一个弱事实,并将其重写为强事实。提示:数据集市 mart_payments,键 payment_id,源 stg_payments。

解决方案: 分析:“数据正确”这个短语是一种愿望,而不是事实。它不包含命令,没有指定预期结果,也无法执行。代理可能认为正确是没有执行错误,分析师可能认为没有空值,产品负责人可能认为是保留了 grain。只有当事实包含命令、预期和状态时,才能消除这种歧义。

强事实:

F1 — payment_id 唯一且不为空

  • 命令: dbt test --profiles-dir . --select mart_payments
  • 预期: payment_id 的 unique 和 not_null 测试通过,退出代码为 0。
  • 状态: 已接受。
此外,可以添加有关金额和日期的事实:total_amount ≥ 0,payment_date 在最近 90 天内。

难度: beginner

名称: 决定将哪个事实自动化,哪个保留为手动

问题: 对于数据集市 mart_customer_360,给定一个断言列表。对于每个断言,指出:自动或手动事实,并说明选择理由。断言:1) customer_id 唯一;2) 数据集中没有 PII 字段;3) 添加 product_code 不会破坏 grain;4) total_orders 根据实际取消计算;5) 所有行都填充了 created_at。

解决方案: 1) 自动 — 对 unique 和 not_null 进行 dbt 测试,可形式化。
2) 自动 — singular 测试,检查模型列集是否存在 PII 名称或值。
3) 手动 — 关于在 grain 中接受新字段的决定取决于契约和阶段,需要阅读规范和产品负责人确认。
4) 自动(部分)— singular 测试,将 total_orders 与源的聚合进行比较,并检查是否已考虑取消。此外还有手动事实:评审员确认模型代码中取消处理逻辑。
5) 自动 — 对 created_at 列进行 generic not_null 测试。

难度: intermediate

名称: 在一个事实中链接验证层

问题: 描述一个验证事实如何链接模型规范 (specs/models/mart_customer_360.md)、ODCS、models/schema.yml、singular 测试和评审员报告。展示如果任何层发生变化,事实会发生什么。

解决方案: 事实示例“customer_id 唯一且不为空”:
- 规范固定了 grain 和必需字段列表。
- ODCS 将 customer_id 描述为具有 string 类型的必需字段。
- models/schema.yml 包含对 customer_id 的 unique 和 not_null 测试。
- Singular 测试额外检查在所有连接之后 customer_id 不为 NULL(防止右侧为 NULL 的 LEFT JOIN)。
- 评审员报告记录了测试在 CI 中通过且事实状态为“已接受”。

如果规范发生变化(例如,grain 变为 customer × product),则所有四层都需要重新审视:ODCS 发生变化,在 schema.yml 中唯一性转移到复合键,singular 测试更新,评审员重新检查事实。validation.md 必须明确指出哪些事实已被重新审视。

难度: advanced

案例研究:
名称: Customer 360 数据集市发布:validation.md 如何防止 PII 泄露

场景: 分析团队正在为营销准备数据集市 mart_customer_360 的发布。数据集市是从 stg_customers(包含 email 和 phone)和 stg_orders 收集的。在代码评审中遗漏了一个条件,email 字段意外保留在输出集中。模型规范明确禁止在数据集中包含直接 PII。

挑战: 没有正式的检查,“PII 未进入数据集市”这一事实只存在于分析师的脑海中。代理认为发布成功的依据是 dbt run 的退出代码,分析师认为是没有空值,产品负责人认为是保留了 grain。没有人检查列的组成。

解决方案: 团队将 validation.md 作为发布的必需工件引入。对于 mart_customer_360,编写了带有 singular 测试 assert_customer_360_no_direct_pii 的事实 F2,该测试将模型的列列表与禁止的 PII 字段列表进行比较。该测试通过命令 dbt test --select assert_customer_360_no_direct_pii 运行,应返回 0 行。该事实已包含在发布检查表中,并在出错时阻止部署。

结果: 在下一次试图将 email 带入数据集市的尝试中,测试在 CI 中就已经失败,评审员看到了事实 F2 的违规行为并拒绝了合并。PII 没有泄漏到营销数据集中。发布过程变得可重现:事实可以随时重新运行并获得相同的答案。

经验教训:
没有命令和预期结果的“PII 未进入数据集市”是愿望,而不是检查。

Singular 测试可以防止 generic schema.yml 测试中看不到的回归。

validation.md 应该是可版本控制的工件,而不是聊天中的记录。

相关概念:
PII 防护

Dbt singular 测试

验证事实

手动和自动事实

名称: 没有验证的 Grain 更改:如何损失一个月的报告

场景: 分析师向数据集市 mart_sales 添加了 product_sku 字段,以便仪表板可以按商品显示销售。变更通过了代码评审,order_id 上的 not_null 和唯一性测试仍然为绿色。两周后,财务部门发现收入翻倍:数据集市的 grain 实际上变成了 order_id × product_sku,尽管名称和测试说 grain 是 order_id。

挑战: 项目中没有 validation.md。测试仅检查列的属性,但没有记录“数据集市的 grain — 一个 order_id = 一行”这一事实。没有人明确记录添加 product_sku 会改变 grain。评审员没有收到关于注意什么的提示。

解决方案: 事件发生后,团队为每个数据集市引入了必需的 validation.md。对于 mart_sales,出现了事实 F1(grain:order_id 唯一且 not_null)和手动事实 F5:“评审员确认变更不会在没有所有者明确断言的情况下更改契约字段和 grain”。在添加 product_sku 的工单中,手动事实需要明确的决定:要么更改 grain 和数据集市的名称,要么将 product_sku 放在单独的层中。

结果: 三个月后,团队建议在数据集中添加 channel 字段。评审员阅读了 validation.md,看到了事实 F5 并停止了合并,要求对 grain 做出单独的决定。发布时间缩短了,因为评审是按照评分标准而不是凭直觉进行的。

经验教训:
键的唯一性与固定的 grain 不同。Grain 是一种契约,需要手动检查。

validation.md 在 SQL 之前编写:它限制了解决方案的大小并提示需要哪些测试。

如果 validation_md 在 SQL 之后编写,它就会变成对已完成工作的辩护,而不是对发布的保护。

相关概念:
数据集市的 Grain

手动事实

数据集市契约

SDD

名称: Lineage 作为验证事实:捕获静默的源更改

场景: 平台团队更新了 stg_orders 中的架构,将列 order_total 重命名为 order_total_amount。mart_sales 的 dbt 模型继续构建,测试通过,因为代码中使用了 ref() 并且通过重命名变量自动适应了新名称。但是数据集市 mart_sales_weekly 损坏了:其中一个下游仪表板显示零,因为它使用了硬编码的 SELECT 指定了旧名称。

挑战: Generic 测试没有失败,因为 mart_sales_weekly 是从中间模型构建的,其中列已被重命名。没有记录输入模型及其架构的事实。评审员没有收到源已更改的信号。

解决方案: 团队在每个数据集市的 validation.md 中添加了关于 lineage 的事实 F4:命令 dbt list --select +<mart> --output name 记录前置项列表。此外还引入了 singular 测试,用于检查源中必需列的存在 (information_schema)。当 stg_orders 中的列被重命名时,这样的测试将在构建数据集市时失败,评审员将看到事实 F4 的违规行为。

结果: 一个季度后,团队注意到在 stg_customers 中删除 customer_segment 字段的尝试。源中列存在性的测试失败,validation.md 中的事实 F4 被标记为需要重新审视,在做出明确决定之前,变更未进入 main。

经验教训:
Lineage 是契约的一部分。源组成或架构的更改应在 validation.md 中可见。

对 information_schema 的自动测试可以防止不会破坏 generic 测试的静默重命名。

validation.md 链接多个层:规范、ODCS、schema.yml、singular 测试和评审。

相关概念:
Lineage 和输入模型

Dbt singular 测试

验证事实

数据集市契约

学习建议:
在 SQL 之前编写 validation.md。首先制定事实(grain、PII、契约、lineage、手动评审),然后制定测试和模型。这减少了解决方案的大小并使评审结构化。

将 validation.md 与规范和代码一起保留在 Git 中。聊天或 Wiki 中的文件不能作为发布的证明。

将检查表中的每个弱点转变为强事实:添加命令、预期结果和状态。“看起来正常”不是事实。

不要试图自动化所有内容。依赖于契约的决策(例如,将什么视为活跃客户)应保留为手动事实,并指定要阅读什么以及应得出什么结论。

使用 singular 测试进行无法用 generic 测试表达的检查:列组成、日期范围、缺少 PII、连接后的引用完整性。

当任何层(规范、ODCS、schema.yml、模型、源)发生变化时,请检查 validation.md 并标记需要重新审视的事实。

使用 Qwen(或其他 LLM)起草 validation_md,向其传递 specs/models/、ODCS、ODPS 和 models/schema.yml 的链接。不要让 LLM 更改 dbt 模型 — 仅生成验证事实。

将 validation.md 添加到 pull request 检查表中:评审员应在更改之前阅读该文件并标记已重新审视的事实。

区分 grain(契约)和键的唯一性(测试)。唯一性可以自动检查,grain 的更改只能手动进行,并需要所有者确认。

数据集市的最小事实集:一个关于 grain,一个关于 PII,一个关于契约的必需字段,一个关于 lineage,一个关于未经断言契约不变的手动事实。

附加资源:
Dbt 文档 — tests: https://docs.getdbt.com/docs/build/tests — 有关 dbt generic 和 singular 测试的官方文档。

Dbt 文档 — singular tests: https://docs.getdbt.com/docs/build/singular-tests — 有关使用 SQL 编写 singular 测试的指南。

Odcs (open data contract standard): https://bitol-io.github.io/open-data-contract-spec/ — 与 validation.md 链接的数据契约标准。

Odps (open data product standard): https://opendataproducts.org/ — 在 SDD 中用作规范层之一的数据产品标准。

数据项目的 Specification-driven development:SDD 的概念,其中 validation.md 是必需的发布工件,而不是可选文档。

Dbt-utils 包: https://github.com/dbt-labs/dbt-utils — 包含可在 validation.md 中使用的现成测试(expression_is_true、accepted_range、relationships 等)的包。

摘要: validation.md 是 SDD 教科书变得严格的地方。验证事实与愿望的不同之处在于它可以执行:命令(dbt test)、SQL/singular 测试或记录在案的手动评审步骤。数据集市的最小 validation.md 包含五个事实:grain、PII 防护、契约的必需字段、lineage/输入模型以及关于未经断言契约不变的手动事实。并非所有事实都需要自动化 — 依赖于契约的决策(例如,grain 中新字段的可接受性)仍然是手动的。validation.md 链接多个层:模型规范、ODCS、models/schema.yml、singular 测试和评审员报告。当任何层发生变化时,该文件必须明确显示哪些事实需要重新审视。主要实践:在 SQL 之前编写 validation.md — 这限制了解决方案的大小,提示了所需的测试,并为评审员提供了现成的评分标准。如果 validation.md 在 SQL 之后编写,它就会变成对已完成工作的辩护,而不是对发布的保护。
我的笔记
0 / 10000

笔记保存在当前浏览器中。在其他设备上将不会显示。

课程菜单

课程

SDD Data. 使用 Qwen Code 和 dbt 构建银行数据平台
进度 0 / 110