4、数据契约的Schema定义:字段类型、约束条件、元数据标签、数据血缘标注
数据契约长什么样?说白了,它就是一个结构化的描述文件。你得告诉消费方:我给你什么字段、每个字段是什么类型、有什么限制、打上了哪些业务标签、这数据从哪来的。
我见过太多团队,上来就写契约,结果字段类型写错了,约束条件漏了,下游一接就报错。嗯,咱们今天就把这四块掰开揉碎讲清楚。
4.1 字段类型:别让类型成为第一道坎
字段类型是契约最基础的部分。你想想看,如果生产方说“这个字段是字符串”,消费方按整数去解析,那不乱套了吗?
我个人习惯,在契约里明确写出每个字段的物理类型和逻辑类型。物理类型就是数据库里的真实类型,逻辑类型是业务语义上的类型。
核心原则:物理类型必须精确到长度、精度、是否可为空。逻辑类型要说明业务含义,比如“订单金额(元,保留两位小数)”。
举个例子,一个订单表的契约定义:
{
"fields": [
{
"name": "order_id",
"physical_type": "VARCHAR(64)",
"logical_type": "订单唯一标识",
"nullable": false
},
{
"name": "amount",
"physical_type": "DECIMAL(18,2)",
"logical_type": "订单金额(元)",
"nullable": false
},
{
"name": "created_at",
"physical_type": "TIMESTAMP(3)",
"logical_type": "订单创建时间(UTC+8)",
"nullable": false
}
]
}
我在项目中遇到过,有人把金额字段定义成 FLOAT,结果精度丢失,对不上账。后来我们强制要求金额类字段必须用 DECIMAL,并明确精度。这个坑,踩过一次就记住了。
4.2 约束条件:给数据上把锁
光有类型还不够。你得告诉消费方,这个字段有哪些约束。比如:值不能重复、必须在某个范围内、必须符合某种格式。
约束条件分几类:
- 唯一约束:这个字段的值不能重复,比如订单ID。
- 范围约束:数值必须在某个区间,比如年龄 0-150。
- 格式约束:必须符合正则表达式,比如手机号、邮箱。
- 枚举约束:值只能从指定集合中取,比如状态字段。
- 引用约束:值必须在另一个表中存在,比如用户ID必须在用户表中存在。
我的建议:约束条件不要写太多,否则生产方维护成本高。只写那些“不满足就会出大问题”的约束。比如金额不能为负、状态值必须在枚举范围内。那些“建议性”的约束,写在备注里就好。
看一个带约束的契约示例:
{
"fields": [
{
"name": "status",
"type": "VARCHAR(20)",
"constraints": {
"enum": ["pending", "paid", "shipped", "cancelled"],
"description": "订单状态,只允许这四个值"
}
},
{
"name": "email",
"type": "VARCHAR(256)",
"constraints": {
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
"description": "用户邮箱,必须符合邮箱格式"
}
}
]
}
4.3 元数据标签:给数据打上业务烙印
元数据标签,说白了就是给字段贴标签。比如这个字段属于哪个业务域、什么敏感级别、什么数据分类。
为什么要做这个?我举个例子。你有一个“用户手机号”字段,如果没有标签,消费方可能直接拿去分析。但如果你打上“PII(个人身份信息)”标签,消费方就知道要脱敏处理。
元数据标签通常包括:
- 业务域标签:交易域、用户域、商品域
- 敏感度标签:公开、内部、敏感、高度敏感
- 数据分类标签:PII、财务数据、行为数据
- 生命周期标签:热数据、温数据、冷数据
注意:标签不是越多越好。我曾经见过一个团队,一个字段打了十几个标签,结果没人知道哪个标签是权威的。建议每个字段不超过3个核心标签,标签体系要提前统一。
带标签的契约示例:
{
"fields": [
{
"name": "phone",
"type": "VARCHAR(20)",
"tags": {
"business_domain": "user",
"sensitivity": "highly_sensitive",
"data_classification": "PII"
}
}
]
}
4.4 数据血缘标注:让数据可追溯
数据血缘,就是告诉你这个字段从哪来、经过了什么处理、最终去了哪。没有血缘的数据,就像没有来源的货物,你敢用吗?
我个人习惯,在契约里标注两个层面的血缘:
- 字段级血缘:这个字段是从上游哪个表的哪个字段来的,经过了什么转换。
- 表级血缘:这个表依赖哪些上游表,又会被哪些下游表使用。
看一个字段级血缘的标注:
{
"fields": [
{
"name": "user_level",
"type": "VARCHAR(20)",
"lineage": {
"source": "ods.user_behavior.user_id",
"transform": "CASE WHEN total_amount > 10000 THEN 'VIP' ELSE '普通' END",
"upstream_table": "ods.user_behavior",
"downstream_tables": ["app.user_profile", "bi.user_analysis"]
}
}
]
}
避坑指南:我曾经在项目里发现,血缘标注和实际ETL代码对不上。原因是ETL改了,但契约没更新。后来我们强制要求:每次ETL变更,必须同步更新契约中的血缘信息,否则CI/CD不通过。
4.5 知识体系总览
下面这张图,把数据契约Schema的四个核心维度串起来了。你可以看到,字段类型是基础,约束条件是规则,元数据标签是语义,数据血缘是溯源。四者缺一不可。
4.6 完整契约示例
最后,咱们看一个完整的契约定义。把上面四块内容整合到一起:
{
"schema_version": "1.0",
"table": "ods.order_info",
"description": "订单信息表,每日全量同步",
"fields": [
{
"name": "order_id",
"physical_type": "VARCHAR(64)",
"logical_type": "订单唯一标识",
"nullable": false,
"constraints": {
"unique": true,
"description": "全局唯一订单号"
},
"tags": {
"business_domain": "trade",
"sensitivity": "internal"
},
"lineage": {
"source": "mysql.order_db.orders.order_id",
"transform": "直接映射"
}
},
{
"name": "user_phone",
"physical_type": "VARCHAR(20)",
"logical_type": "用户手机号",
"nullable": false,
"constraints": {
"pattern": "^1[3-9]\\d{9}$",
"description": "中国大陆手机号"
},
"tags": {
"business_domain": "user",
"sensitivity": "highly_sensitive",
"data_classification": "PII"
},
"lineage": {
"source": "mysql.user_db.users.phone",
"transform": "直接映射,下游使用需脱敏"
}
}
]
}
总结一下:字段类型要精确到长度和精度,约束条件要必要且可验证,元数据标签要统一且克制,数据血缘要真实且及时更新。这四块做好了,你的数据契约才算真正能用。