数据契约的 Schema 定义:字段类型、约束条件、元数据标签、数据血缘标注

好,咱们进入正题。上一章我们把数据契约的骨架搭起来了,知道它长什么样、谁来签、怎么流转。但骨架有了,还得有血肉——也就是 Schema 定义。

说白了,Schema 就是数据契约的「身份证」。它告诉所有人:这个数据集里有哪些字段,每个字段是什么类型,能填什么值,不能填什么值,从哪来的,要往哪去。我见过太多团队,契约签得挺热闹,结果 Schema 写得稀里糊涂,上线第一天就出事故。

这一章,我就把 Schema 定义的四个核心要素掰开揉碎讲清楚:字段类型、约束条件、元数据标签、数据血缘标注。

核心观点:Schema 不是 DDL 的复制品,而是数据契约的「语义层」。它既要机器能解析,也要人能读懂。

数据契约 Schema 四要素关系图 数据契约 Schema 字段类型 约束条件 元数据标签 数据血缘标注 四者缺一不可:类型定格式,约束定边界,标签定语义,血缘定来源

3.1 字段类型:别让「字符串」背所有的锅

字段类型是 Schema 最基础的部分。但基础不代表简单。我见过一个项目,所有字段都用 string,结果下游做聚合计算时全崩了——因为金额字段里混了「待确认」这样的文本。

我个人习惯,定义字段类型时遵循三个原则:

  • 精确到子类型:别只写 number,要写 decimal(18,2) 或 int。日期别写 string,写 date 或 timestamp。
  • 枚举值显式声明:状态字段用 enum('active','inactive','deleted'),别让下游猜。
  • 空值语义明确:null 和空字符串 '' 是两回事,必须在契约里写清楚。

我的小技巧:在契约里加一个「类型示例」字段。比如 amount 字段,示例写 1234.56,比写一百字文档都管用。

来看一个实际例子。这是我在一个金融项目中用过的 Schema 片段:

{
  "fields": [
    {
      "name": "transaction_id",
      "type": "string",
      "format": "uuid",
      "description": "交易唯一标识",
      "nullable": false
    },
    {
      "name": "amount",
      "type": "decimal(18,2)",
      "description": "交易金额,单位元",
      "nullable": false,
      "example": "1234.56"
    },
    {
      "name": "status",
      "type": "enum('pending','success','failed','refunded')",
      "description": "交易状态",
      "nullable": false
    },
    {
      "name": "created_at",
      "type": "timestamp",
      "format": "yyyy-MM-dd HH:mm:ss",
      "description": "创建时间",
      "nullable": false
    }
  ]
}

你想想看,如果下游拿到这样的 Schema,还需要去翻文档吗?不需要。字段类型本身就把所有信息说清楚了。

3.2 约束条件:给数据上「紧箍咒」

字段类型管的是「格式对不对」,约束条件管的是「值合不合理」。我在项目中遇到过最惨的一次事故:一个年龄字段没加范围约束,结果上游传了个 -1 过来,下游的报表直接炸了。

约束条件我一般分四类:

约束类型 说明 示例
范围约束 数值或日期的上下限 age: [0, 150]
枚举约束 值必须在指定集合内 status: ['active','inactive']
格式约束 正则或特定格式要求 phone: /^1[3-9]\d{9}$/
依赖约束 字段间的逻辑关系 end_date > start_date

注意:约束不是越多越好。我曾经见过一个契约,一个字段加了 8 条约束,结果数据生产方天天报错。约束要「够用就好」,先保核心,再逐步收紧。

约束条件怎么写进契约?我推荐用 JSON Schema 标准,它天然支持这些:

{
  "name": "age",
  "type": "integer",
  "minimum": 0,
  "maximum": 150,
  "description": "用户年龄"
},
{
  "name": "email",
  "type": "string",
  "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
  "description": "邮箱地址"
},
{
  "name": "start_date",
  "type": "date",
  "description": "开始日期"
},
{
  "name": "end_date",
  "type": "date",
  "description": "结束日期,必须晚于开始日期",
  "dependentRequired": {
    "start_date": ["end_date"]
  }
}

嗯,这里要注意:dependentRequired 这种依赖约束,很多工具不支持自动校验。我建议在契约里用 description 字段写清楚逻辑,再配合自动化测试来兜底。

3.3 元数据标签:给数据打上「语义标签」

字段类型和约束条件解决的是「机器可读」的问题。但数据最终是给人用的,人需要知道这个字段「是什么意思」——这就是元数据标签的活。

说白了,元数据标签就是给字段贴「小纸条」。比如:

  • 业务分类:这个字段属于「用户信息」还是「交易信息」?
  • 敏感级别:是 PII(个人身份信息)还是公开数据?
  • 数据来源:是埋点采集的,还是从第三方接口拿的?
  • 质量等级:这个字段的准确率是 99.9% 还是 95%?

我习惯在 Schema 里加一个 tags 字段,用键值对的形式存储:

{
  "name": "phone_number",
  "type": "string",
  "tags": {
    "business_domain": "用户域",
    "sensitivity": "PII",
    "data_source": "CRM系统",
    "quality_tier": "gold",
    "steward": "张三"
  }
}

我的经验:标签不要超过 10 个,否则维护成本太高。优先保证「敏感级别」和「业务域」这两个标签,其他按需添加。

元数据标签还有一个妙用:自动化治理。比如,所有带 sensitivity: PII 标签的字段,在数据导出时自动脱敏。你想想看,这比人工一个个去配规则省多少事?

3.4 数据血缘标注:让数据「有据可查」

最后这个,是 Schema 定义里最容易被忽略、但最重要的部分。数据血缘标注,就是告诉所有人:这个字段从哪来,经过了什么处理,要往哪去。

我曾经接手过一个数据平台,里面有个字段叫 user_score,没人知道它是怎么算出来的。问了一圈,有人说从 A 表来的,有人说从 B 表来的,最后查了三个月代码才发现是 C 表。这种经历,一次就够了。

数据血缘标注在契约里怎么做?我推荐两种方式:

  1. 字段级血缘:标注每个字段的上游来源表和字段。
  2. 转换逻辑:标注字段经过了什么计算或映射。

看一个实际例子:

{
  "name": "total_amount",
  "type": "decimal(18,2)",
  "lineage": {
    "source": "ods.order",
    "source_field": "amount",
    "transform": "SUM(amount) GROUP BY user_id",
    "etl_job": "dwd_user_order_daily",
    "updated_at": "2024-01-15 10:30:00"
  }
}

关键点:血缘标注不是一次性的。数据链路变了,契约里的血缘信息必须同步更新。我建议把血缘信息纳入 CI/CD 流程,每次数据管道变更自动触发契约更新。

这里有个常见的坑:血缘信息写得太粗。比如只写「来自订单表」,但订单表有 50 个字段,到底哪个?一定要精确到字段级别。我曾经见过一个团队,血缘写的是「来自用户中心」,结果排查问题时根本没法用。

3.5 把四要素串起来:一个完整的 Schema 示例

好,前面四个要素都讲完了。咱们把它们拼起来,看一个完整的 Schema 长什么样:

{
  "schema_version": "1.0",
  "dataset": "dwd_user_order_daily",
  "fields": [
    {
      "name": "user_id",
      "type": "string",
      "format": "uuid",
      "nullable": false,
      "description": "用户唯一标识",
      "tags": {
        "business_domain": "用户域",
        "sensitivity": "PII",
        "quality_tier": "gold"
      },
      "lineage": {
        "source": "ods.user",
        "source_field": "user_id",
        "transform": "直接映射"
      }
    },
    {
      "name": "order_count",
      "type": "integer",
      "minimum": 0,
      "nullable": false,
      "description": "用户当日订单数",
      "tags": {
        "business_domain": "交易域",
        "sensitivity": "internal"
      },
      "lineage": {
        "source": "ods.order",
        "source_field": "order_id",
        "transform": "COUNT(DISTINCT order_id) GROUP BY user_id"
      }
    },
    {
      "name": "total_amount",
      "type": "decimal(18,2)",
      "minimum": 0,
      "nullable": false,
      "description": "用户当日交易总额",
      "tags": {
        "business_domain": "交易域",
        "sensitivity": "internal"
      },
      "lineage": {
        "source": "ods.order",
        "source_field": "amount",
        "transform": "SUM(amount) GROUP BY user_id"
      }
    }
  ]
}

你看,这样一个 Schema,机器能解析、人能读懂、问题能追溯。这才是一个合格的数据契约该有的样子。

最后提醒一句:Schema 定义不是一次性的工作。数据在变,业务在变,契约也得跟着变。我建议每个契约都带上版本号,每次变更都要走审批流程。别嫌麻烦——今天省的事,明天都会变成事故找回来。