主题: 第 16 部分。Customer 360 数据集市
难度等级: 中级
预计学习时间: 4-6 小时
前置要求: 对 SQL 和数据模型的基础理解
熟悉 dbt(data build tool)及 ref() 概念
理解数据集市(data marts)的构建原则
了解 SDD(规范驱动开发,Specification-Driven Development)的概念
对 PII(个人可识别信息,Personally Identifiable Information)及个人数据处理原则的基础理解
具有使用 YAML 文件和 markdown 文档的经验
学习目标: 解释 mart_customer_360 集市作为项目中第一个最终数据集市的目的和架构
对照 SQL 实现、模式、规范和验收事实,以发现覆盖率的缺口
应用验收矩阵,以具体证据类型验证每个验收事实的达成情况
识别并防止 PII 字段从 staging/raw 层泄漏到最终的 mart
构建 dbt 测试(unique、not_null、singular)以证明验收事实
概述: 课程第 16 部分专注于 mart_customer_360 集市——项目中的第一个最终数据集市,它整合了客户属性、账户余额和卡片活动。本节是核心的教学案例,展示了产品规范、ODCS(Open Data Contract Standard)、模型规范、dbt 测试和验收事实如何协调一致。特别强调产品纪律:集市被刻意限制——每个客户一行、无直接 PII、余额从账户计算、风险事件取自合成活动窗口。这种限制使示例「诚实」:每个字段都可以关联到规范和验证。学生学习理解为什么字段名(例如 risk_event_count_7d)会成为契约的一部分,以及为什么 PII 政策不能停留在笼统的表述上。Customer 360 数据集市是理解 SDD(规范驱动开发)原则的起点,其中就绪状态的证明建立在验收矩阵之上,而不是 SQL 的可视化评审之上。
关键概念: Mart customer 360: 项目中的第一个最终数据集市(mart),整合了客户属性、账户余额和卡片活动。是规范、实现和测试整合的主要教学案例。限定为每个 customer_id 一行。
Приёмочные факты (acceptance facts): 关于集市的可验证的具体声明,必须由证据覆盖。包括:每个 customer_id 一行、customer_id not null 且 unique、无直接 PII、total_balance_rub not null、risk_event_count_7d not null、契约必备字段存在、lineage 可通过 ref() 读取。
Матрица приёмки (acceptance matrix): 将每个验收事实与具体证据(dbt 测试 unique、not_null、模式 singular 测试、契约列 singular 测试)相对照的表格。如果证据行为空,则不是就绪,而是缺口。
Odcs (open data contract standard): 用于正式描述产品规范的数据契约标准。在集市上下文中——文件 specs/customer_360_contract.odcs.yaml,描述对数据结构与数据质量的承诺。
Sdd (specification-driven development): 在 SQL 代码之前先编写规范的方法。SDD 强制选择产品承诺的唯一版本并提前记录,从而降低契约漂移的风险。
Dbt-тесты: dbt(data build tool)中的自动化检查,用作验收事实的证据。包括 generic 测试(unique、not_null)和 singular 测试(用于检查模式、契约列、缺少 PII 的任意 SQL)。
Lineage через ref(): 通过 dbt 中的 ref() 函数读取数据来源(data lineage)的能力。其中一个验收事实:lineage 必须透明且可重建。
Pii (personally identifiable information): 可识别主体的个人数据。SDD 中的 PII 政策不能是笼统的表述:在 raw 和 staging 层中,出于教学目的允许 pii_email,但在 mart 中不应出现。这是产品的边界,而不是模式美观的问题。
Risk event count 7d: 七天窗口内的风险事件计数字段。字段名本身即承诺了七天窗口,因此如果 SQL 仅统计所有风险事件,则该字段看似合理但会误导消费者。窗口必须在描述中说明并通过单独的测试加以验证。字段名是契约的一部分。
Дрейф контракта (contract drift): 已声明的规范与实际实现之间的偏离。必须设有单独的检查,而不是仅通过 SQL 的可视化评审来发现。
Доказательство приёмки: 确认验收事实达成的具体工件(dbt 测试、模式评审、显式验证事实、评审者报告)。没有证据,就绪状态不予认定。
Заметка ревьюера (reviewer note): 评审工件的最小输出,包含:粒度、PII 状态、契约字段、检查清单和未决问题。示例:「粒度:每个 customer_id 一行。PII:无直接 PII。契约字段:customer_id、total_balance_rub、risk_event_count_7d。检查:dbt build、mart 测试、禁止 PII 列表、契约列。未决问题:risk_event_count_7d 的定义依赖于输入风险标志」。
Qwen-запрос для ревью: 用于 LLM 助手(Qwen)的典型提示,用于对照 SQL、模式、ODCS 和验证文件,以发现已覆盖、手动和缺失的验收事实。在此过程中不修改文件。
练习题: 名称: 验收矩阵审计
问题: 您接手了 mart_customer_360 集市。实习学生声称一切就绪,因为 dbt build 已成功通过。请进行审计:遍历每个验收事实,并将其与验收矩阵中的证据相对照。确定哪些事实已自动覆盖,哪些需要人工评审,哪些根本没有证据。文件:models/marts/mart_customer_360.sql、models/schema.yml、specs/customer_360_contract.odcs.yaml、specs/validation/customer_360.md。
解决方案: 第 1 步。列出规范中的所有验收事实:(1) 每个 customer_id 一行,(2) customer_id not null,(3) customer_id unique,(4) 无直接 PII,(5) total_balance_rub not null,(6) risk_event_count_7d not null,(7) 必备契约字段存在,(8) lineage 可通过 ref() 读取。第 2 步。打开 models/schema.yml。找到 customer_id、total_balance_rub、risk_event_count_7d 的 unique 和 not_null 测试。记录:事实 (2)、(3)、(5)、(6) 已由 generic 测试覆盖。第 3 步。检查是否存在 singular 测试,列出禁止的 PII 字段(pii_email、pii_phone 等)并与 mart 列列表进行比较。如果存在测试——事实 (4) 已自动覆盖;如果不存在——标记为需要人工评审。第 4 步。找到契约列的 singular 测试(检查 mart 中确实存在 customer_id、total_balance_rub、risk_event_count_7d 以及 ODCS 中的其他必备字段)。第 5 步。检查 SQL 中仅对上游模型使用 ref()——否则事实 (8) 未覆盖。第 6 步。生成覆盖率表。如果至少有一行为空,则是就绪状态的缺口,而非就绪。
难度: 中级
名称: 编写关于缺少 PII 的 singular 测试
问题: staging 层中存在 pii_email 字段(用于教学目的和风险演示)。请为 dbt 编写一个 singular 测试,检查在最终数据集市 mart_customer_360 中该字段缺失,并且不包含任何其他列名中含子串 'email'、'phone'、'passport'、'inn' 的列。
解决方案: 创建文件 tests/mart_customer_360_no_pii.sql,内容如下:
{{ config(severity='error') }}
with mart_columns as (
select column_name
from information_schema.columns
where table_schema = 'marts'
and table_name = 'mart_customer_360'
),
forbidden_substrings as (
select unnest(array['email', 'phone', 'passport', 'inn', 'snils']) as forbidden
)
select m.column_name, f.forbidden
from mart_columns m
cross join forbidden_substrings f
where lower(m.column_name) like '%' || f.forbidden || '%'
将该文件放置于 tests/ 文件夹下(或放置于 models/marts/ 中并配置 {{ config(materialized='test') }})。测试应返回行——如果结果为空,则不存在 PII 字段。在 schema.yml 中添加测试描述:description: "检查 mart_customer_360 中不存在直接 PII 字段"。这是一个模式 singular 测试,作为验收事实「无直接 PII」的证据。
难度: 中级
名称: 保护 risk_event_count_7d 窗口
问题: risk_event_count_7d 字段承诺风险事件的七天窗口。mart_customer_360.sql 中的当前实现对来自 stg_risk_events 的所有事件进行计数,不按日期筛选。请编写一个验证测试,(a) 在 SQL 中固定窗口,(b) 添加 dbt 测试,证明该计数器确实相对于加载日期限制为最近 7 天。
解决方案: 第 1 步。修正 SQL 模型 mart_customer_360.sql,使计数仅在窗口内:添加条件 event_date >= current_date - interval '7 days' 或使用 dbt_date 宏。代码片段示例:count(case when r.event_date >= current_date - interval '7 days' then 1 end) as risk_event_count_7d。第 2 步。创建 singular 测试 tests/mart_customer_360_risk_window.sql,将 mart 中的计数与 stg_risk_events 中窗口内的参考计数进行比较:
{{ config(severity='warn') }}
with mart as (
select customer_id, risk_event_count_7d
from {{ ref('mart_customer_360') }}
),
stg_window as (
select customer_id,
count(*) as expected_7d
from {{ ref('stg_risk_events') }}
where event_date >= current_date - interval '7 days'
group by 1
)
select m.customer_id
from mart m
join stg_window s on m.customer_id = s.customer_id
where m.risk_event_count_7d <> s.expected_7d
第 3 步。在规范 specs/validation/customer_360.md 中记录窗口定义:「risk_event_count_7d — 相对于集市计算日期的客户最近 7 个日历日内的风险事件数量」。第 4 步。在 schema.yml 中的 risk_event_count_7d 字段添加指明窗口的描述。现在,字段名、SQL、测试和规范已一致。
难度: 中级
名称: 通过 ref() 校验 lineage
问题: 审查员怀疑在 mart_customer_360.sql 中某个上游模型未通过 ref() 连接,而是通过 source() 直接引用源。请找出此违规,解释风险并提出修复建议。
解决方案: 第 1 步。打开 models/marts/mart_customer_360.sql。检查 with 块和 from 子句中的所有调用:应仅包含 {{ ref('stg_customers') }}、{{ ref('stg_accounts') }}、{{ ref('stg_cards') }}、{{ ref('stg_risk_events') }}。第 2 步。查找 {{ source('...', '...') }} 的调用——这表示绕过 staging 层直接访问 raw。第 3 步。解释风险:source() 绕过转换层,破坏 data lineage,使 mart 之前的数据质量检查无法进行,并导致契约漂移。第 4 步。将 source() 替换为对应 staging 模型的 ref()。第 5 步。运行 dbt compile 并确认 lineage 构建正确(dbt docs generate → 打开可视化)。第 6 步。添加 singular 测试,扫描 SQL 中是否存在 source()(可选,进阶)。
难度: 中级
名称: 编写评审者备注
问题: 根据课程材料中的模板,准备 mart_customer_360 集市评审者的最小输出。使用以下输入:集市已通过 dbt build;schema.yml 中存在 customer_id、total_balance_rub、risk_event_count_7d 的 unique 和 not_null 测试;不存在关于 PII 的 singular 测试,但代码中视觉上未选择 pii_email;契约列的 singular 测试存在,但仅检查 ODCS 中 5 个必备字段中的 3 个;SQL 正确使用 ref();specs/validation/customer_360.md 中 risk_event_count_7d 的定义提到了 7 天窗口,但未指定锚定日期。
解决方案: 现成的评审者备注:
粒度:每个 customer_id 一行(已由 dbt 测试 unique 覆盖)。
PII:视觉上不存在,但无自动证据——状态为「人工评审,需要模式 singular 测试」。
契约字段:customer_id、total_balance_rub、risk_event_count_7d(部分覆盖——按 ODCS 为 5 个字段中的 3 个,存在 2 个字段缺口)。
检查:dbt build — ok,mart 测试 unique/not_null — ok,契约列 singular 测试 — 不完整,禁止 PII 列表 — 缺失。
未决问题:
1. risk_event_count_7d 的定义未固定锚定日期(current_date vs event_date vs 集市计算日期)——存在漂移风险。
2. ODCS 中还有哪些 2 个字段应存在于 mart 中?需要与 customer_360_contract.odcs.yaml 核对。
3. PII 检查仅作为可视化评审存在——需要添加 singular 测试。
结论:集市尚未具备生产就绪状态。需要在合并前关闭 3 个未决问题。
难度: 中级
案例研究: 名称: pii_email 泄漏到银行营销集市
场景: 一家大型零售银行为 CRM 部门构建 Customer 360 数据集市。营销团队请求「客户的所有可用信息」以进行细分和个性化。marts 层的 dbt 模型开发人员拼装了一张包含 47 列的表,其中包括 pii_email 和 pii_phone,理由是这些字段「已在 stg_customers 中存在,且群发邮件需要用到」。集市通过了仅评审代码的审查:审查员查看 SQL,看到整洁的 join,但未将列列表与 PII 政策和 ODCS 契约核对。
挑战: 4 个月后,安全部门进行审计,发现营销集市包含按内部规章应在 mart 层屏蔽或哈希的直接 PII。该集市已用于 12 个仪表板和 3 个客户流失预测 ML 模型。数据未实际泄漏(访问受限),但监管风险已实现:发现违反个人数据处理政策,银行面临央行的整改指令。
解决方案: 团队引入 SDD 方法:(1) 重写 ODCS 契约 customer_360_contract.odcs.yaml,明确列出允许和禁止的字段;(2) 从 mart 中删除 pii_email 和 pii_phone,替换为 hash_email 以进行安全细分;(3) 添加 singular 测试 mart_customer_360_no_pii.sql,扫描 information_schema,并在列名出现禁止子串时失败;(4) 在 PR 模板中引入验收矩阵的强制步骤——审查员必须填写事实覆盖率表;(5) 接入 Qwen 助手,在每个 PR 中对比 SQL、schema.yml 和 ODCS,并突出差异。
结果: 引入 SDD 后 6 个月内,mart 层中涉及 PII 的事件数量降至零。集市评审的平均时间增加了 15 分钟,但回滚和返工次数减少。监管风险已关闭:银行在复审中无异议。团队将该实践推广到其他 14 个集市。
经验教训: 仅评审代码是不够的——需要自动 singular 测试来证明 PII 政策的执行。
ODCS 契约应是 mart 字段组成的唯一真实来源,而非审查员的视觉印象。
诸如「Customer 360」之类名称看起来浅显易懂——不同利益相关者有不同的期望;SDD 固定了承诺的唯一版本。
验收矩阵将抽象的「就绪」转化为带有证据的事实离散列表。
相关概念: mart_customer_360
PII(个人可识别信息)
ODCS(Open Data Contract Standard)
验收矩阵
模式 singular 测试
SDD(规范驱动开发)
名称: 保险公司 risk_event_count_30d 契约漂移
场景: 一家保险公司使用 Customer 360 数据集市进行欺诈评分。risk_event_count_30d 字段承诺 30 天窗口。数据分析师对 stg_risk_events 进行了修改:添加了筛选条件 where event_status = 'confirmed',以去除「原始」告警。从技术上讲,该字段仍是「30 天内的风险事件」,但其语义从「所有告警」偏移到「已确认告警」。mart_customer_360 中的 SQL 未变更,dbt build 通过,无人注意到该变化。
挑战: 2 个月后,评分模型开始产生误报性的保单拦截:仅有未确认告警的客户现在未被拦截,而应当被拦截。季度欺诈损失上升 8%。调查发现,mart 仍将该字段命名为 risk_event_count_30d,但其语义已发生变化。契约和实现不知不觉地产生了偏离。
解决方案: 团队引入多层防御:(1) 在 specs/validation/customer_360.md 中添加正式定义:「risk_event_count_30d — 相对于集市计算日期,在 stg_risk_events 中 event_status in ('confirmed', 'investigating') 的最近 30 天内的记录数」;(2) 在 mart_customer_360.sql 中硬性固定筛选条件 event_status in ('confirmed', 'investigating') 并添加注释;(3) 创建 singular 测试,将 mart 中的字段值与按固定公式计算的参考计数进行比较——任何偏离都会使构建失败;(4) 在 schema.yml 中为该字段添加引用规范章节的描述。
结果: 评分模型恢复至预期语义,欺诈损失回归目标水平。引入正式契约测试后,此类事件从每年 2-3 起降至零。团队形式化了一条规则:「如果字段名包含窗口、状态、货币或聚合级别——这是契约的一部分,需要单独的测试」。
经验教训: 字段名是契约的一部分。上游层的隐性修改会在不知不觉中漂移下游集市的语义。
dbt build 通过 ≠ 就绪。需要专门检查语义的测试,而不仅仅是结构。
staging 层的筛选条件会改变 mart 字段的语义,即使 mart 中的 SQL 未变更。需要端到端测试。
窗口定义必须包含锚定日期以及事件的纳入/排除条件。
相关概念: risk_event_count_7d
契约漂移
验证规范
singular 测试
作为契约一部分的字段名
名称: 一个季度内将 14 个集市迁移到 SDD
场景: 一家金融科技初创公司在 marts 层拥有 14 个集市,由不同的开发人员在 2 年内编写完成。契约方法各异:部分集市有 ODCS,部分仅有 wiki 页面,另一些则完全没有文档。集市评审完全手动,每个耗时 2-3 天。数据质量不可预测:消费者抱怨「漂移的」指标,Customer 360 数据集市在不同分支上重新构建时结果不同。
挑战: 产品总监要求在一个季度内稳定质量并缩短新集市的上市时间。由 5 名 dbt 开发人员组成的团队需在不中断数据交付的同时将现有集市迁移到 SDD。高风险:迁移过程中可能破坏已运行多年的功能。
解决方案: 团队采用增量方法:(1) 选择 mart_customer_360 作为试点——它是「第一个最终集市」,业务逻辑清晰,字段数量有限;(2) 按第 16 部分的描述,为试点编写 ODCS 契约、模型规范、验证规范和验收矩阵;(3) 引入 3 种类型的 dbt 测试:必备字段的 unique/not_null、关于缺少 PII 的 singular 测试、对应契约列的 singular 测试;(4) 通过 Qwen 助手自动化检查:PR 触发评审,对比 SQL、schema.yml 和 ODCS,并生成事实覆盖率表;(5) 试点成功后,将工件模板化并按每周 2 个的速度推广至其余 13 个集市。
结果: 12 周内,14 个集市全部迁移到 SDD。由于自动覆盖率检查,代码评审时间从 2-3 天缩短至 4-6 小时。数据漂移事件数从每季度 8-10 起降至半年 1 起。消费者反馈,集市变得「可预测」——文档与实现不再脱节。额外收获:新开发人员的入职时间从 1 个月缩短至 1 周,得益于标准化的模板。
经验教训: Customer 360 是 SDD 的理想试点:名称为人熟知,因此每个人都认为自己理解,但不同的人有不同的期望。书面规范消除了歧义。
验收矩阵将抽象的「就绪」转化为带有证据的具体清单。证据行为空 = 就绪状态存在缺口。
LLM 助手(Qwen)加速常规事实覆盖率审查,但不能替代人类对未决问题的决策。
工件模板化(ODCS、规范、验证)可加速推广并降低集市之间的差异。
相关概念: SDD(规范驱动开发)
ODCS(Open Data Contract Standard)
验收矩阵
Qwen 评审提示
评审者备注
契约漂移
学习建议: 从验收矩阵开始,而非 SQL。在阅读代码之前,遍历每个事实并找到证据——这会将视角从「模型做了什么」转变为「模型承诺了什么」。
为每个集市建立「事实 → 证据 → 测试类型」表。空格单元格 = 就绪状态缺口。这是您的主要工作工件。
对于名称中带有「承诺」的每个字段(7d、30d、confirmed、rub、count),请自问:「此承诺在代码中的何处被固定?」如果仅在名称中,则属于技术债。
使用课程中的 Qwen 提示作为自有 PR 的模板:对比 SQL、schema.yml、ODCS 和验证文件,输出覆盖率表。不修改文件——仅进行分析。
不要局限于阅读 SQL。打开 information_schema 并检查 mart 中物理上不存在 PII 列,即使在 SELECT 中看不到它们。
立即编写 singular 测试,而非「以后再说」。缺少 PII 的测试只需 10 行代码,却能节省数周的调查时间。
对每个 PR 填写课程模板中的评审者备注。这能强化纪律:关注事实,而非印象。
运行 dbt build --select mart_customer_360 并逐行分析输出——哪一行对应矩阵中的哪个事实。
不要将「视觉上不存在 PII」与「已证明不存在 PII」混淆。前者是评审,后者是测试。SDD 要求证据。
在窗口类字段中使用日期锚定:始终固定相对于哪个日期计算窗口(current_date、加载日期、event_date)。
附加资源: dbt 测试文档: https://docs.getdbt.com/docs/build/data-tests — 关于 generic 测试(unique、not_null、accepted_values、relationships)和 singular 测试的官方指南。
dbt ref() 文档: https://docs.getdbt.com/reference/dbt-jinja-functions/ref — ref() 函数及 data lineage 构建的说明。
ODCS(Open Data Contract Standard): https://bitol-io.github.io/open-data-contract-standard/ — SDD 中使用的数据契约标准规范。
dbt-utils 包: https://github.com/dbt-labs/dbt-utils — 宏集合,包括针对 PII 和契约列的测试(expression_is_true、relationships 等)。
sqlfluff dbt linter: https://sqlfluff.com/ — dbt 模型 SQL 代码的静态分析,有助于保持风格一致并发现基本错误。
dbt-expectations 包: https://github.com/calogica/dbt-expectations — 借鉴 Great Expectations 风格的 dbt 扩展测试。
《data contracts》一书(andrew jones): 关于作为产品工件的数据契约的概念性介绍。有助于理解 SDD 为何有效。
Great expectations: https://greatexpectations.io/ — 数据验证框架,其概念与 dbt singular 测试相呼应。
摘要: 第 16 部分「Customer 360 数据集市」是 SDD(规范驱动开发)应用于最终数据层的核心教学案例。mart_customer_360 集市被刻意限制:每个 customer_id 一行、无直接 PII、余额来自账户、风险事件来自合成窗口。每个字段都可以关联到规范和验证。核心思想:验收矩阵将抽象的「就绪」转化为带有具体证据(dbt 测试 unique/not_null、模式和契约列的 singular 测试、通过 ref() 的 lineage 检查)的事实离散列表。如果证据行为空,则是就绪状态缺口,而非就绪。字段名是契约的一部分:如果名称包含窗口(7d)、状态或聚合级别,则该承诺必须在代码中固定并通过测试验证。PII 政策不能是笼统的表述——它通过自动 singular 测试验证,扫描 mart 的列名。工件的最小输出是评审者备注,包含粒度、PII 状态、契约字段清单、检查清单和未决问题。Qwen 助手可以加速常规事实覆盖率检查,但不能替代人类的决策。主要教训:在 SDD 中,仅仅编写正确的 SQL 是不够的——需要通过测试或显式事实证明每一项承诺。