数据契约的 Schema 定义:字段类型、约束条件、元数据标签、数据血缘标注
好,咱们进入正题。上一章我们把数据契约的骨架搭起来了,知道它长什么样、谁来签、怎么流转。但骨架有了,还得有血肉——也就是 Schema 定义。
说白了,Schema 就是数据契约的「身份证」。它告诉所有人:这个数据集里有哪些字段,每个字段是什么类型,能填什么值,不能填什么值,从哪来的,要往哪去。我见过太多团队,契约签得挺热闹,结果 Schema 写得稀里糊涂,上线第一天就出事故。
这一章,我就把 Schema 定义的四个核心要素掰开揉碎讲清楚:字段类型、约束条件、元数据标签、数据血缘标注。
核心观点:Schema 不是 DDL 的复制品,而是数据契约的「语义层」。它既要机器能解析,也要人能读懂。
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 表。这种经历,一次就够了。
数据血缘标注在契约里怎么做?我推荐两种方式:
- 字段级血缘:标注每个字段的上游来源表和字段。
- 转换逻辑:标注字段经过了什么计算或映射。
看一个实际例子:
{
"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 定义不是一次性的工作。数据在变,业务在变,契约也得跟着变。我建议每个契约都带上版本号,每次变更都要走审批流程。别嫌麻烦——今天省的事,明天都会变成事故找回来。