主题:第9部分. Raw 和 Bronze 落地
难度级别:中级
预计学习时间:2-3 小时(理论 + 实践)
先修知识: 对 Data Lakehouse 架构的基础理解(raw、bronze、silver、gold 层)
具备 dbt 或类似数据转换工具的使用经验
熟悉 SQL 及数据模式相关概念
理解 lineage 和元数据原则
具备 Python 基础知识(用于生成合成数据)
学习目标: 区分 Raw 和 Bronze 层的职责范围,并说明在每一层上允许进行哪些转换
解释为何在没有明确业务依据的情况下不能在 raw 层修改、删除或隐藏数据
编写验收事实(acceptance facts),以验证数据入仓的可复现性
通过源清单而非手工修改来描述空值和 null 的语义
构建 raw → 清单 → 规范 → staging → 测试 的证据链,以保护产品决策
概述:第9章专门讨论 Lakehouse 架构的前两层——Raw 和 Bronze——它们负责数据入仓的可复现性。在教学示例中,raw 由脚本 scripts/generate_data.py 生成的 CSV 文件表示;在生产环境中,它可以是对象存储中的落地区、Kafka 主题、CDC 表或外部 API。尽管物理实现方式不同,但原则只有一个:在第一步不能改变数据的含义。Raw 回答「来了什么」这个问题,并按原样保存输入——包括空值、PII 和矛盾之处。Bronze 添加最少的技术规范化(文件格式、类型化、加载元数据、lineage),但不做出产品决策。本章的目标是教会读者区分对数据的描述与对数据的修改,理解「平淡而诚实的」raw 是防止未来关于数据来源争议的保险(在银行场景下尤为关键),并构建一条简短的证据链,使任何审阅者都能从源头追溯到数据集市。
核心概念: Raw 层:回答「来了什么」这一问题的落地区。按其原始形式保存数据:相同的字段名、相同的顺序、相同的空值和 null 标记。禁止重命名字段、删除行、隐藏 PII、在没有证据的情况下修改值,以及执行任何聚合操作。Raw 层有意做得「平淡」:它的任务是成为源的诚实镜像,而不是过滤器或规范化器。
Bronze 层:中间技术层,仅添加最少量的规范化,不涉及产品逻辑:文件格式(Parquet、Delta)、字段的基本类型化、加载元数据(时间、源、batch_id)、列存在性检查以及基本 lineage。Bronze 不应做出业务决策——例如,决定空的 revoked_at 表示同意有效。这一决策转移到 staging 规范中。
入仓可复现性:Lakehouse 的关键属性:能够重新生成 raw 数据,并沿着转换链一直追溯到集市,得到相同的结果。通过生成脚本的版本控制、源清单、表头校验以及合成数据的固定化来实现。
Raw/Bronze 验收事实:确认输入正确的最少检查集合:源文件存在且可访问,表头与来自域映射的预期源模式匹配,合成数据的生成是可复现的,空值和 null 的语义已在清单中描述,PII 在处理策略批准之前没有丢失。
空值语义:关于如何解释空单元格和 NULL 值的规则,记录在源清单中,而不是代码或数据中。例如,空的 revoked_at 可能意味着「同意有效」或「在该字段实施之前同意已被撤销」——选择由业务做出并明确记录,而不是为了方便测试通过而默认假定。
Lineage 和加载元数据:在 Bronze 层添加的技术信息:源名称、文件或主题的路径、加载时间戳、批次标识符、模式版本。允许追溯集市中的任何一行直至原始事件。
源清单:记录 raw 数据预期结构和语义的文档:列名及其类型、空值和 null 标记的描述、业务所有者、更新策略、关于争议字段的开放问题。将数据源与其余架构连接起来。
Sdd data(规范驱动数据):一种方法,其中关于数据的决策记录在规范和清单中,而不是转换代码中,更不用说在数据本身中了。Raw/Bronze 是 SDD 保护未来与审阅者或监管机构对话的地方。
「绿灯测试」的典型错误:为了通过 dbt build 而修改原始 CSV 的诱惑。表面上错误消失了,但同时消失的是源发送了模糊值的证据。正确的路径:固定输入,在 staging 契约中描述解释方式,添加测试。
域映射:描述领域对象、它们之间的关系、数据所有者以及预期源的领域映射。在核对 raw 文件的表头时用作参照。
重要日期: Lakehouse 演进阶段(大约 2019-2020):Databricks 中出现 medallion 架构(Bronze/Silver/Gold)术语,并确立了通过分层方式处理数据的想法,明确分离技术规范化和业务逻辑。
dbt 在 sdd 实践中的采用(自 2021 年起):dbt 作为一种工具的流行度上升,它使 staging 层和 mart 层「用 SQL 对话」,加强了对模型规范和测试的关注,从而提升了诚实 raw 层的重要性。
关于 PII 的监管变化(GDPR 自 2018 年起、俄罗斯 фз-152 的更新):对个人数据处理的要求更加严格,这使得「在策略批准之前不隐藏 PII」的规则在银行和金融领域变得尤为关键。
实践练习: 标题:查找具有模糊空值语义的字段
问题:打开 book3/examples/bank-lakehouse/raw 目录并研究所有 CSV 文件。找一个同时出现空字符串和显式 null 标记(或看起来像 'unknown'、'n/a' 等的值)的字段。对于找到的字段,请描述:(1) 它应如何作为 raw 事实保存;(2) 在 Bronze 中将如何以及在哪里执行技术类型化;(3) 解释规则将出现在哪个 staging 规范中;(4) dbt 中的哪个测试应验证解释的正确性。提示:从 consents.csv 文件和 revoked_at 字段开始。
解答:步骤 1. 打开 raw/consents.csv 并找到 revoked_at 为空的行。步骤 2. 固定 raw 事实:空单元格按原样保留为空(或在 Pandas/duckdb 读取后变为 NULL,取决于解析器),不进行默认值替换。步骤 3. 在 Bronze 模型 stg_consents(或等效模型)中执行到 DATE 或 TIMESTAMP 的类型转换,不解释空值的含义。步骤 4. 在 staging 规范中(例如在 schema.yml 或 _models/_docs 中)添加规则:在 staging 逻辑中,空的 revoked_at 被解释为「导出时同意有效」。步骤 5. 编写 dbt 测试,验证转换后对于所有有效同意,is_active = true 的值符合预期比例(例如 not_null 或 accepted_values)。步骤 6. 在测试说明中记录决策,解释为何选择这种解释方式。
难度:中级
标题:核对表头与域映射
问题:使用 Qwen(或手动方式),执行「Qwen 查询」部分中的请求:检查 book3/examples/bank-lakehouse/raw 中的 raw 源,将表头与域映射中预期的源进行比较,并编制一份源事实和开放问题的列表。不要更改 dbt 模型。目标是在差异导致 staging 错误之前将其记录下来。
解答:步骤 1. 打开 raw 目录中的每个 CSV,并写出实际的表头。步骤 2. 打开域映射(例如仓库中的 docs/domain_map.md 或类似文件),并写出每个源的预期表头。步骤 3. 编制差异表:哪个字段缺失、哪个被重命名、哪个具有意外的类型。步骤 4. 对于每个差异进行分类:(a) 源的真实错误 → 向所有者提出开放问题;(b) 术语差异 → 澄清清单;(c) 领域模型已更改 → 更新映射。步骤 5. 编制一份名为「源事实与开放问题」的 markdown 文档,成为审阅者的入口点。
难度:中级
标题:反模式:修复 CSV 而不是修复契约
问题:模拟一个场景:您运行 dbt build,stg_consents 模型失败并报错「NULL value not allowed for column revoked_at_iso」。在 raw 文件中确实存在 revoked_at 为空的行。模拟两种解决方案路径——错误的和正确的——并描述每种方案在 1、6 和 12 个月后的后果。
解答:错误路径:打开 CSV,将空字符串替换为占位日期(例如 1900-01-01),重新保存文件,重新运行 dbt build——它变为绿色。1 个月后没人记得这次替换。6 个月后审计人员询问为什么系统中有 12% 的同意具有 1900 年的撤销日期。12 个月后您无法恢复哪些客户真正撤销了同意,哪些没有。正确路径:按原样固定 raw;在 staging 模型中使用 safe_cast 或 try_cast 将空值转换为 NULL;在清单中描述空的 revoked_at 被解释为「同意有效」;在派生字段 is_active 上添加 dbt 测试 not_null,并添加 is_active ↔ revoked_at 一致性测试。在 1、6 和 12 个月后,证据链保持完整:raw → 清单 → staging → 测试。
难度:中级
标题:为新源设计验收事实
问题:团队正在接入一个新源——来自外部处理系统的卡片操作导出。源每天以 parquet 文件的形式到达 S3。为 raw 层和 bronze 层设计一套将被自动验证的验收事实(例如 Great Expectations、dbt-source-freshness 或自定义 Python 脚本)。
解答:Raw 验收事实:(1) 文件存在于预期路径 s3://bucket/raw/card_ops/yyyy/mm/dd/ 下;(2) 文件大小 > 0;(3) 表头与清单匹配(card_id、txn_id、amount、currency、txn_ts、mcc);(4) 校验和与清单匹配(防止替换);(5) 没有意外的列;(6) PII 字段(card_id)存在且未被掩盖。Bronze 验收事实:(1) 模式已转换为预期类型(amount → DECIMAL,txn_ts → TIMESTAMP);(2) 添加了加载元数据(loaded_at、source_batch_id、source_file_path);(3) raw 和 bronze 的行数一致(防止转换过程中的丢失);(4) freshness:数据不早于 N 小时;(5) lineage 已写入 meta 表。所有检查都在 dbt build 启动之前运行,并在失败时阻止其继续。
难度:高级
案例研究: 标题:导出后半年关于客户同意的争议
场景:一家零售银行收到客户的投诉:「我在 3 月就撤销了对个人数据处理的同意,但 8 月他们还在打电话向我推销营销信息。」法务部要求数据团队提供从 3 月到 8 月每天的同意状态确认。数据来自外部 CRM 的每日 CSV 导出,存放在 raw 层,然后通过 dbt 转换为同意集市。
挑战:在 3 月 CSV 文件的初始加载期间,revoked_at 字段部分是空的(源在过渡期间发送数据,部分系统尚未传输撤销日期)。为了使 dbt build 绿色通过,团队打开了文件并将空单元格替换为 1900-01-01 的日期。从技术上讲,数据看起来「干净」,但在 3 月中旬真正撤销了同意的客户现在直到 8 月仍被列为有效——因为系统中的「撤销」发生在 1900 年。
解决方案:数据团队的审计恢复了事件链:在 git 历史中找到提交 CSV 文件被重新保存并替换了日期的提交;检查 staging 模型并发现 revoked_at = 1900-01-01 → is_active = false 的解释未被记录。团队重做流程:(1) 从带版本控制的 S3 备份恢复原始 CSV;(2) 在源清单中固定空的 revoked_at 被解释为「无数据」,而不是「有效」;(3) 在 staging 逻辑中添加显式规则:is_active = (revoked_at IS NULL AND consent_given_at IS NOT NULL);(4) 添加禁止在生产数据中出现 1900-01-01 日期的 dbt 测试;(5) 重新发布同意集市,并向法务部提供正确的时间线。
结果:法务部获得了正确的时间线:客户确实在 3 月 14 日撤销了同意,并且从该日期起他不应再收到营销沟通。银行在没有罚款的情况下解决了争议。内部流程被重新审视:禁止任何对 raw 文件的手动编辑,在 CI 中添加检查,确保 CSV 在初始提交后的 git 历史为空,所有空值的解释都经过清单审查。
经验教训: 「绿色测试」不是正确性的证明;修改源数据替换了信号并破坏了可复现性
空值语义应存在于规范中,而不是代码或数据本身
在银行场景下,争议不是今天发生的,而是几个月后——因此 raw → 清单 → staging → 测试 的证据链必须随时可恢复
Raw 数据的版本控制(S3 版本控制、DVC、git-lfs)是一种保险,允许回退「手工修复」并查看源实际发送了什么
相关概念: Raw 层
空值语义
源清单
SDD Data
「绿色测试」的典型错误
入仓可复现性
标题:将 Kafka 主题作为新的 raw 源接入
场景:团队从批量 CSV 导出过渡到通过 Kafka 流式接收支付事件。主题 payments.events 通过 Kafka Connect 以 Avro 格式写入对象存储。团队必须决定 raw 和 bronze 之间的边界在哪里,以及谁负责 Avro 模式。
挑战:出现了新问题:(1) 由 Kafka Connect 生成的 S3 中的 parquet 文件是否应被视为 bronze 层(因为其中包含模式和元数据),还是仍属于 raw?(2) 是否需要为审计保留原始 Avro 字节流?(3) 在流式数据中如何描述空值的语义,其中「事件缺失」本身就是一个事实?
解决方案:团队做出以下决定:每个批次的原始 Avro 快照保存在 S3 中 raw/payments_avro/ 目录下,用于审计目的——这才是真正的 raw。由 Kafka Connect 生成的 parquet 文件放在 raw/payments/ 目录下,并被视为 raw 事实:其中包含 Kafka 的技术元数据(offset、partition、timestamp),但没有业务解释。Bronze 模型 payments_bronze 仅执行:(1) 将 Avro 模式转换为 dbt 类型;(2) 添加 loaded_at、source_partition;(3) 检查必填字段的存在性(event_id、amount、currency);(4) 校验 Avro 快照中的记录数与 parquet 中的记录数一致。空值语义在清单中固定:空的 merchant_category 表示「MCC 未由源系统确定」,而不是「业务决定类别缺失」。
结果:团队获得了两个独立的证据层:用于深度审计的 Avro 快照和用于日常转换的 parquet。在集市和源之间出现任何差异时,可以在数小时内恢复 Kafka 实际发送了什么。Avro 快照的存储成本被降低的监管风险所抵消。
经验教训: 物理文件格式(Parquet、Avro、CSV)不会自动决定层——重要的是数据在管道中的角色
即使下游工具(Kafka Connect、Fivetran)已添加技术元数据,Raw 仍应保留
在流式数据中,「事件缺失」与事件本身一样重要,并且应在清单中描述
Bronze 不应承担解释源模式的责任——这是 staging 规范的任务
相关概念: Bronze 层
Lineage 和加载元数据
Raw/Bronze 验收事实
入仓可复现性
域映射
标题:CDC 表作为 raw:技术性与产品性之间的边界
场景:团队从 PostgreSQL 转向 CDC(Change Data Capture):Debezium 将变更写入主题,数据从那里进入 Iceberg 表 raw.customers_cdc。该表包含所有操作(INSERT、UPDATE、DELETE),并带有元数据 __op、__ts_ms、__source_ts_ms。分析师只需要客户的最新版本。
挑战:「只保留客户的最新版本」这条规则应该存在于 bronze 还是 staging?如果在 bronze,则 bronze 开始做出产品决策。如果在 staging,则 staging 接收操作流并需要处理 CDC 语义。此外,还需要决定如何处理 DELETE 操作——是将它们视为「客户删除」还是「源系统中的操作」。
解决方案:团队固定:raw.customers_cdc 按原样存储所有操作,无需过滤。这意味着表中保留了 INSERT、UPDATE、DELETE,甚至 TOAST 更新。Bronze 模型 customers_cdc_bronze 仅执行:(1) 显式类型转换(特别是 PostgreSQL 中的 JSON 字段);(2) 将技术性 CDC 字段重命名为 lineage 字段(cdc_op、cdc_ts_ms);(3) 添加 loaded_at、source_db、source_table。关于「最新版本」、「有效客户」、「是否应用 DELETE」的所有决策都转移到 staging 规范 stg_customers 中,在那里它们被显式描述并由测试覆盖。例如,测试检查应用 CDC 操作后有效客户的数量是否与运营系统匹配。
结果:团队获得了「平淡的」raw,其中可以看到源的所有细微差别:例如,发现 UPDATE 操作有时先于 INSERT 到达(由于 Kafka 分区的顺序),这需要在 staging 中使用特殊逻辑。如果没有 raw 事实,这种异常将被「干净的」bronze 层隐藏,并仅在与监管机构就余额重新计算的正确性发生争议时才会显现。
经验教训: CDC 数据始终是 raw,因为它反映源的内部机制,而不是产品表示
「实体的最新版本」是 staging 决策,应记录在案并由测试覆盖
CDC 的技术元数据(__op、__ts_ms)作为 lineage 保留很重要,而不是在 bronze 阶段丢弃
Raw 层保护在更换 staging 逻辑时不丢失上下文:如果明天业务更改「什么算作有效客户」的规则,可以在不丢失原始操作的情况下重新计算集市
相关概念: Raw 层
Bronze 层
Lineage 和加载元数据
SDD Data
入仓可复现性
学习建议: 在阅读本章之前,打开 book3/examples/bank-lakehouse/raw 目录并快速浏览所有 CSV 文件——这有助于看到文本中讨论的真实空值和矛盾示例。
维护一个单独的笔记本「Raw 开放问题」,在阅读过程中记录所有争议字段。此笔记本将成为您自己项目中源清单的基础。
大声复述规则:「Raw 回答来了什么的问题,staging 回答这意味着什么的问题。」如果在课堂上您忘记了边界——回到这句话。
在学习案例时,先尝试自己提出「错误路径」,然后与作者的对比——这样可以更有效地形成反模式,在生产中您就不会忍不住去修改 CSV。
使用「反向 lineage」技术:从集市中的任何字段向上追溯到 raw 文件,并检查每个转换是否都已记录。如果链条在某处中断——这是一个信号,表明 bronze 或 raw 中存在隐藏的产品逻辑。
为自己制定一个个人规则:「在修改文件之前,问自己——这是描述数据还是修复数据?」如果是修复——停下来,重新审视 staging 契约。
为自己制作一份包含 5 个 raw 验收事实的清单,并贴在显示器旁边——这将把抽象要求转化为接纳新源时的日常习惯。
为了记住 raw/bronze 的区别,使用助记符:「Raw 是镜子,Bronze 是镜子周围的画框。」画框可以改善外观,但不应改变映像。
补充资源: 书籍《fundamentals of data engineering》(joe reis、matt housley,o'reilly 2022):关于存储和接入的章节详细解释了为什么第一层应该是「平淡的」,以及 raw、bronze 和不变性原则之间的联系。
dbt 文档:「about dbt project structure」和「source freshness」:描述如何在 dbt 中正确声明 sources 并检查 freshness——这是 raw 层验收事实的形式化。
文章「the medallion architecture: a guide to bronze, silver, and gold layers」(databricks):medallion 架构的原始描述,重点关注层之间的边界以及禁止在 bronze 中使用业务逻辑。
「great expectations」(或替代品 soda core)资料:用于将 raw/bronze 验收事实形式化为代码的工具,使检查可版本化且可复现。
书籍《designing data-intensive applications》(martin kleppmann,关于流处理的章节):深入解释什么是「事件到达的事实」以及为什么将其与解释分开很重要。
俄罗斯 фз-152 标准和欧盟 gdpr 关于 pii 处理的规定:解释为什么在策略批准之前不能在 raw 中隐藏 PII 的监管基础——这不仅是工程规范,也是法律规范。
Dbt-utils 包:source freshness 和 accepted values 测试:在标准 dbt 项目中将部分 raw/bronze 验收事实形式化的现成测试。
Qwen(或类似的 llm 助手):用于自动化 raw 文件表头与域映射之间例行检查的工具——本章给出了查询示例。
总结:第9章「Raw 和 Bronze 落地」阐述了三个关键原则。首先,raw 是「平淡而诚实的」层,按其原始形式保存输入:不重命名字段、不删除行、不隐藏 PII、不修改值、不聚合。它的任务是成为源的镜像,而不是过滤器。其次,bronze 添加最少的技术规范化(文件格式、类型化、加载元数据、lineage),但不应做出产品决策——对争议值的解释转移到 staging 规范中。第三,入仓可复现性通过验收事实进行验证:文件存在、表头正确、生成可复现、空值语义已描述、PII 在策略批准之前保持完整。主要的典型错误是为了「绿色测试」而修改 raw 数据,这表面上消除了问题,但破坏了证据链,并使得数月后无法恢复真相(在银行场景下尤为关键)。学完本章后,读者应能够区分对数据的描述与对数据的修改,为新源设计验收事实,并构建一条简短的、可恢复的 raw → 清单 → 规范 → staging → 测试 链。