跳至主要内容

什么是数据合约?入门指南与示例

在分布式数据系统中实现可扩展性并减少错误。
Mike Shakhomirov
Mike Shakhomirov

创建您的免费账户

实践并学习路径你的进步。

使用 Google 继续继续即表示您接受我们的 使用条款,我们的 隐私政策 ,并且您的数据存储在美国。

数据合约是分布式数据解决方案实现数据质量与可扩展性的基石。它们规定数据库实体之间交换所遵循的格式、模式和协议。这些正式约定消除了关于数据的歧义和未成文假设。

本文将通过提供基础与进阶技巧,帮助您澄清数据合约的概念并有效落地实施。

理解数据合约

单个数据合约界定两个模型之间数据交换的精确参数。这些正式约定确保数据格式与模式没有任何歧义。

数据合约的定义与验证对于跨团队高效协作至关重要。

简而言之,数据合约是变更我们数据原始状态的过程(生产者)与目标方(消费者)之间的正式协议。这与商业合同非常相似:它们代表供应商与消费者之间的义务。数据合约对数据产品(如表、视图、数据模型等)起到同样的作用。

其目标是缓解数据管道下游的中断,使数据转换稳定且可靠。

数据合约的主要组成包括模式(列与格式)、语义层(度量、计算与限制)、服务级别协议(SLA)以及数据治理。

数据合约的好处包括:

  • 数据质量的自动化与检查,用于新数据输出的创建或更新。
  • 支持高效扩展,尤其适用于分布式数据架构,例如数据网格。
  • 改进数据开发生命周期,重点建设合约验证工具。
  • 通过生产者与消费者之间的反馈促进协作。

解释数据合约如何工作的图片。

数据合约。图片由作者提供。

使用 dbt 的数据合约示例

在数据合约中,模式定义属性名、数据类型以及属性是否必填。它们还可为列指定格式、长度及可接受值范围。

来看一个在 YAML 文件中定义的 dbt 模型模式。我们的表模式在 columns 下定义:

models:
 - name: dim_orders
   config:
     materialized: table
     contract:
       enforced: true
   columns:
     - name: order_id
       data_type: int
       constraints:
         - type: not_null
     - name: order_type
       data_type: string

现在假设我们将 dim_orders 模型定义为:

select
 'abc123' as order_id,
 'Some order type' as order_type

在模型定义中将 contract 设为 enforced: true,当我们尝试在数据平台中将 dim_orders 物化为表时,会触发如下错误:

20:53:45  Compilation Error in model dim_customers (models/dim_orders.sql)
20:53:45    This model has an enforced contract that failed.
20:53:45    Please ensure the name, data_type, and number of columns in your contract match the columns in your model's definition.
20:53:45
20:53:45    | column_name | definition_type | contract_type | mismatch_reason    |
20:53:45    | ----------- | --------------- | ------------- | ------------------ |
20:53:45    | order_id    | TEXT            | INT           | data type mismatch |
20:53:45
20:53:45
20:53:45    > in macro assert_columns_equivalent (macros/materializations/models/table/columns_spec_ddl.sql)

如果我们选择定义额外列、SLA 检查或缺失元数据,也会出现类似情况。

更高级的 dbt 示例会包含强制的模型约束:

# models/schema.yaml
models:
 - name: orders
  
   # required
   config:
     contract:
       enforced: true
  
   # model-level constraints
   constraints:
     - type: primary_key
       columns: [id]
     - type: FOREIGN_KEY # multi_column
       columns: [order_type, SECOND_COLUMN, ...]
       expression: "OTHER_MODEL_SCHEMA.OTHER_MODEL_NAME (OTHER_MODEL_FIRST_COLUMN, OTHER_MODEL_SECOND_COLUMN, ...)"
     - type: check
       columns: [FIRST_COLUMN, SECOND_COLUMN, ...]
       expression: "FIRST_COLUMN != SECOND_COLUMN"
       name: HUMAN_FRIENDLY_NAME
     - type: ...
  
   columns:
     - name: FIRST_COLUMN
       data_type: DATA_TYPE
      
       # column-level constraints
       constraints:
         - type: not_null
         - type: unique
         - type: foreign_key
           expression: OTHER_MODEL_SCHEMA.OTHER_MODEL_NAME (OTHER_MODEL_COLUMN)
         - type: …

以一组规则和约束来提供数据集列的模式定义,为数据处理与分析提供了非常重要的信息。

模式往往会随时间变化。

这是常见情形。设想源表新增了一列合约中的字段:

select
 'abc123' as order_id,
 'Some order type' as order_type,
 'USD' as currency

在执行增量更新时务必考虑模式变更,否则下游增量模型的输出会使合约失效。

可通过在 dbt 增量策略中添加 on_schema_change: append 来解决。

模式校验可以是显式的,也可以是隐式的。

一些大数据文件格式(如 AVRO 和 Parquet)默认支持内置模式与隐式模式定义,因此无需额外的外部验证。

相反,无模式的数据文件格式(如 JSON)需要外部模式验证。某些 Python 库(如 pydantic 或简单的 @dataclass)可以完成这项工作:

from pydantic import BaseModel
class ConnectionDataRecord(BaseModel):
   user: str
   ts: int
record = ConnectionDataRecord(user="user1", ts=123456789)

如果我们违反规则,赋予不符合标准的值,将抛出异常。例如,若尝试调用 ConnectionDataRecord('', 1),就会引发异常。

语义数据合约

语义数据验证确保数据在逻辑上一致,并与业务逻辑保持一致。

语义验证必须被显式强制执行。

确实,不同于模式检查,语义数据合约依赖业务逻辑,必须在外部实现。

在很多用例中,语义看起来像模式校验合约的扩展。它通常取决于业务规则,并表现为我们的数据模型必须遵守的一组行级条件。

一个语义合约的示例如下:

  • 指标偏离: 相对于移动平均或其他阈值的偏差。例如,昨日活跃用户数低于7日移动平均值的75%尚可接受,但跌至 0% 则不可接受。
  • 业务逻辑: 交易监控告警标记与反欺诈评分。在此场景中,付款应为 0。
  • 数据沿袭: 描述数据实体如何演化。例如,transaction_completed_at 不可能早于 created_at。
  • 参照完整性: 实体关系很重要,也可用语义合约来描述。请看下面的 dbt 代码。它说明每个退款的 refund_id 都应映射回有效的 transaction.id。
- name: refunds
   enabled: true
   description: An incremental table
 columns:
     - name: refund_id
       tests:
         - relationships:
             tags: ['relationship']
             to: ref('transactions')
             field: id

由终端用户自行设定语义合约的告警级别,这通常通过自定义数据质量测试来实现。

各类业务逻辑规则常涉及数据完整性。非逻辑性数据情形可能源于数据库与服务器配置错误,或测试数据无意注入生产环境。数据完整性检查旨在依据这些业务规则验证数据,帮助识别不合理或不合逻辑的值。

数据库实体总是彼此相关。这称为实体关系,通常以实体关系图(ERD)展示。参照完整性不足会导致缺失或不完整的数据。数据合约必须应对这些潜在陷阱,以确保数据完整性与准确性。

例如,常见关系是 customers 与 orders 之间的一对多关系,即单个客户可能拥有多个 orders。在这种情形下,订单仅当其包含 customers 数据集中有效的 customer_id 时才被视为有效。

这种约束称为参照完整性约束,确保实体间关系被准确维护。

数据合约中的服务级别协议(SLA)

SLA 是可通过数据合约实现的额外数据质量检查层。

SLA 关注数据新鲜度。

由于数据模型会定期更新新数据,SLA 可能包含关于新数据预计可用的最晚时间或最大允许延迟的检查。例如,在 dbt 中,可通过新鲜度测试实现:

- name: orders
   enabled: true
   description: A source table declaration
   tests:
     - dbt_utils.recency: # https://github.com/dbt-labs/dbt-utils#recency-source
         tags: ['freshness']
         datepart: day
         field: timestamp
         interval: 1

假设我们要生成关于昨天事实的报告;为此,我们要确保数据已存在。确实,几天甚至更久都看不到新订单会很奇怪。

在下面的示例中,我们可以使用 dbt 的测试配置来检测数据集中意外的订单延迟。这样您就可以用特定的名称选择和引用该测试。

version: 2
models:
 - name: orders
   columns:
     - name: status
       tests:
         - accepted_values:
             name: unexpected_order_status_today
             values: ['placed', 'shipped', 'completed', 'returned']
             config:
               where: "order_date = current_date"

通过定义自定义名称,您可以完全掌控测试在日志消息与元数据工件中的显示方式。

同样地,在实时数据管道中,通常期望数据不超过数小时之久。对于以分钟甚至秒级延迟处理数据的流式处理应用,SLA 至关重要。

在流式应用中,我们希望检查迟到事件的最大允许延迟,以及平均无故障时间(MTBF)与平均修复时间(MTTR)等指标。实现这一点需要细致地跟踪事件,并从应用监控与事件管理工具(如 PagerDuty、Datadog 和 Grafana)中提取相关数据。

数据治理合约

妥善处理可识别个人身份信息(PII)是数据转换过程不可或缺的一环。对许多公司而言,确保这些数据集至少符合 GDPR,并遵守 HIPAA 或 PCI DSS 等数据隐私法规,至关重要。

数据治理合约确保已建立充分的假名化或数据掩码策略。

请看下面的 dbt 代码。作为 tests 强制的合约要求 user_email 为 SHA256 哈希(已掩码):

models:
 - name: customer_data
   columns:
       - name: user_email
         tests:
           - dbt_expectations.expect_column_values_to_not_match_regex:
               regex: "^(?!.*\b@\b).* # Ensure identifiers do not contain emails
               flags: i # Case-insensitive matching

另一方面,我们可能希望对其他表强制执行模式匹配。例如,在下面的例子中,transaction_reference 字段必须符合 [“TRX-%”, “%-2023”] 模式:

models:
 - name: transaction_data
   columns:
       - name: transaction_reference
         tests:
           - dbt_expectations.expect_column_values_to_match_like_pattern_list:
               like_pattern_list: ["TRX-%", "%-2023"]
               match_on: any

当包含敏感数据的列、元数据(数据所有者等)以及获授权访问某数据产品的用户角色被纳入其中时,数据治理合约也会非常有用。

例如,我们可以使用 dbt 中的 meta 字段指明数据模型的所有者与模型开发生命周期阶段:

# models/schema.yaml
version: 2
models:
 - name: users
   meta:
     owner: "@data_mike"
     model_maturity: in dev
     contains_pii: true
   columns:
     - name: email
       meta:
       contains_pii: true

meta 字段允许您为资源设置元数据,随后会被编译进 dbt 生成的 manifest.json 文件,并可在自动生成的文档中查看。

我们可以使用诸如 dbt-checkpoint 等包在拉取请求期间进行扫描,以确保必填的 meta 字段已存在。若任何模型(来自 manifest 或 YAML 文件)缺少指定的 meta 键,钩子会失败。

模型的 meta 键必须存在于 YAML 文件或 manifest 中。

数据合约的实现模式

在流式数据管道中(按行),可以在数据摄取前或之后——即在源端(源数据模型层)执行合约验证。

当我们“原样”摄取数据时,验证过程就像一次转换步骤,强制执行合约规则并过滤无效数据,例如写入到专门的数据库视图或表,以便进一步调查。

当数据直接摄入原始数据湖后,也可以事后应用验证检查。

实时数据合约验证的主要优势在于,它能在数据到达最终目的地(数据湖或数据仓库)之前过滤掉无效记录。这种方法在事件驱动或实时数据处理中十分常见,例如变更数据捕获(CDC)事件。在这种验证中,随着数据流经管道,会对合约的某些方面进行校验。

数据合约工具

数据社区正逐步认识到数据合约的潜在益处——这是一个不断演进的数据工程领域。该领域已有众多工具问世,部分仍处于早期阶段。dbt 可被视为数据合约的通用框架。

在源端(源数据模型层)也可使用 Google 的 Dataform 实现类似合约,即当数据已成功摄入数据仓库时。我们希望为此使用简单的行级条件。

请看下面的示例。它为我们的表应用了特定的行级条件:

-- my_table.sqlx
config {
 type: "table",
 assertions: {
   nonNull: ["user_id", "customer_id", "email"]
 }
}
SELECT …

下面是另一个数据合约实现的示例,可通过 Soda.io(一个小众的数据质量框架)实现:

# Checks for basic validations
checks for dim_customer:
 - row_count between 10 and 1000
 - missing_count(birth_date) = 0
 - invalid_percent(phone) < 1 %:
     valid format: phone number
 - invalid_count(number_cars_owned) = 0:
     valid min: 1
     valid max: 6
 - duplicate_count(phone) = 0

通过调整告警设置,您可以将检查配置为发出警告而非使验证失败。Soda 扫描会运行协议中指定的检查,检查 YAML 文件或在程序调用中内联的检查,并为每项检查返回结果:通过、失败或错误。

根据我的经验,许多公司引入数据合约的方式仍较为零散,这取决于数据管道设计模式(批处理或实时)、数据序列化选择以及用于存储和处理的系统等因素。

数据合约的实施取决于业务需求与业务逻辑。

Great Expectations 是一个 Python 库,可用于实现语义层级的数据合约。可以使用 pip 安装:pip install great_expectations。

运行 great_expectations init 后,我们即可进行数据验证:

Using v3 (Batch Request) API
 ___              _     ___                  _        _   _
/ __|_ _ ___ __ _| |_  | __|_ ___ __  ___ __| |_ __ _| |_(_)___ _ _  ___
| (_ | '_/ -_) _ |  _| | _|\ \ / '_ \/ -_) _|  _/ _ |  _| / _ \ ' \(_-<
\___|_| \___\__,_|\__| |___/_\_\ .__/\___\__|\__\__,_|\__|_\___/_||_/__/
                               |_|
            ~ Always know what to expect from your data ~
Let's create a new Data Context to hold your project configuration.
Great Expectations will create a new directory with the following structure:
   great_expectations
   |-- great_expectations.yml
   |-- expectations
   |-- checkpoints
   |-- plugins
   |-- .gitignore
   |-- uncommitted
       |-- config_variables.yml
       |-- data_docs
       |-- validations
OK to proceed? [Y/n]:

请看下面的代码片段。它说明了如何为 price 列创建数据检查定义:

"expectation_type": "expect_column_values_to_match_regex",
"kwargs": {
 "column": "price",
 "mostly": 1.0,
 "regex": "^\\$([0-9],)*[0-9]+\\.[0-9]{2}$"
},

数据合约最佳实践

并不存在放之四海皆准的答案。根据我的经验,成功实施数据合约高度依赖业务需求。要让数据合约有效发挥作用,请遵循以下关键最佳实践:

  • 可扩展性: 建议加入可扩展机制(例如 on_schema_change: append)与版本管理,使您能在不破坏现有管道集成的情况下修改合约条款。 以面向未来变更与可扩展性的思路设计数据合约。 这种方法能确保合约适应不断演变的需求与增长。
  • 清晰规则: 建议在数据合约中使用直白、简洁的语言,避免误解与曲解。合约应以所有相关方(无论技术背景如何)都能访问并理解的方式撰写与命名。
  • 协作: 协作努力能确保对需求形成全面、均衡的理解,兼顾多种视角与需要。在创建数据合约时,应邀请多元干系人参与,包括数据生产者、数据工程师、数据科学家,以及来自业务、IT、法务与合规部门的代表。
  • 元数据: 建议为数据合约提供完善的文档与元数据。完善的文档有助于合约的顺利应用与维护。应包含详细说明、字段定义、验证规则及其他相关信息,以支持清晰理解与高效实施。
  • 定期评审: 持续评审有助于长期保持合约的相关性与有效性。为监控和更新数据合约制定结构化方法。定期评审可确保合约保持最新,符合不断演进的业务需求,并遵守新出台或变化的法规。

结论

近期,数据所有权正在向分布式转变,领域团队对其数据产品负责。这一变化促使组织通过数据合约重新定义数据质量预期并将其形式化。

语义数据验证确保数据逻辑一致并符合业务逻辑。它们有助于检查数据管道中的各类异常,例如数值、沿袭与参照完整性。不足的参照完整性会导致数据缺失或不完整,因此同样重要。

数据治理合约也可在 CI/CD 管道中实施,并在标注包含敏感数据的列、元数据(数据所有者等)以及获授权访问数据产品的用户角色时发挥重要作用。

模型的元数据以及开发者定义的其他必填元字段,有助于监控模型的资源使用与性能。

数据合约中的服务级别协议(SLA)明确了与数据新鲜度、完整性与故障恢复相关的具体承诺。

数据合约是现代数据建模技术的要素,有助于确保数据平台稳健可扩展并减少错误。解决数据质量问题可能代价高昂。对于企业级公司而言,通过利用支持数据合约验证的现有数据质量工具,最大化数据投资回报率(ROI)变得愈发重要。

FAQs

数据合约与数据验证、数据测试有何不同?

数据合约是正式协议,规定数据生产者与消费者的数据格式、模式和交换协议。它们为数据质量建立清晰的期望与职责。相对地,数据验证与测试是检查数据是否满足这些期望的过程。验证确保数据符合定义的合约,而测试用于评估数据的准确性与可靠性。

数据合约能否用于实时流式数据管道?

是的,数据合约可以在实时流式数据管道中实施。它们有助于在数据到达目标之前过滤无效数据,确保只处理符合预定义规则与格式的数据。该方法在事件驱动或实时处理场景中尤为有益,因为数据完整性与时效性至关重要。

当模式频繁变化时,如何在数据合约中处理版本管理?

在频繁变更模式的情况下处理数据合约的版本管理,需要保持向后兼容并跟踪模式演进。一种做法是使用版本化的模式,即每个合约版本与特定的模式版本关联。在 dbt 中,on_schema_change: append 等工具可帮助在不干扰现有集成的情况下管理模式变更,从而实现渐进式过渡与更新。

数据合约在数据网格架构中扮演什么角色?

在数据网格架构中,数据所有权分散在各领域团队;数据合约确保跨领域的一致数据质量与互操作性。它们将生产者与消费者之间的期望形式化,帮助团队在数据标准上达成一致,降低数据在不同领域流动时出现质量问题的风险。

数据合约如何帮助遵守 GDPR 或 HIPAA 等隐私法规?

数据合约可以强制执行数据治理策略,包括 GDPR 或 HIPAA 等隐私法规。它们可以定义数据掩码、假名化与访问控制等规则,确保敏感数据得到妥善处理。将这些规则嵌入合约后,组织就能自动化合规检查,降低隐私泄露风险。

Mike Shakhomirov

作者

Mike Shakhomirov

热爱数字化并专注于此,我在数字营销的挑战中如鱼得水。

在移居英国之前,我在销售、公司银行风险与数字营销领域积累了十余年的经验,形成了在风险管理、数学建模、统计分析、工商管理以及市场营销方面的专业能力。

在完成纽卡斯尔的MBA后,我渴望投身于数据驱动营销、计算机科学或人工智能方向,并有机会继续攻读博士学位。这些领域兼具科学的实际应用、持续的职业发展、创新性,以及为充满活力的行业做出贡献的机会。

主题