3. 元数据模型设计:核心实体的定义与关系策略
元数据模型,说白了就是数据目录的骨架。骨架搭歪了,后面再怎么填肉都别扭。我在好几个项目里都见过这种场景——模型设计时图省事,把所有东西塞进一张大表,结果查询性能崩了,关系也理不清。
这一章,我们就来聊聊数据目录里最核心的四个实体:数据源、表、字段、ETL任务。以及它们之间该怎么关联,属性该怎么设计。
3.1 核心实体概览
先看一张整体结构图,帮你快速建立认知。
嗯,这张图很直观。从上到下是数据源 → 表 → 字段的层级关系,ETL任务则横跨多个表和字段,形成数据血缘。
3.2 数据源(DataSource)模型
数据源是元数据的根节点。我见过有人把数据源和连接信息混在一起,结果换一个数据库地址,所有关联的表都要重新录入——这显然不合理。
我个人习惯把数据源拆成两层:基础信息和连接配置。基础信息是业务视角,连接配置是运维视角。
3.2.1 基础属性
| 属性名 | 类型 | 说明 |
|---|---|---|
| datasource_id | UUID | 全局唯一标识 |
| name | string | 业务名称,如"生产MySQL-订单库" |
| type | enum | MySQL / PostgreSQL / Hive / Kafka / HDFS |
| environment | enum | dev / test / staging / prod |
| owner | string | 负责人或团队 |
| description | text | 业务描述 |
| status | enum | active / deprecated / deleted |
3.2.2 连接配置(独立存储)
连接配置我建议单独一张表,和数据源是 1:1 关系。原因很简单:连接信息是敏感数据,需要加密存储,访问权限也要严格控制。
-- 连接配置表结构示例
CREATE TABLE datasource_connection (
connection_id UUID PRIMARY KEY,
datasource_id UUID NOT NULL REFERENCES datasource(datasource_id),
host VARCHAR(255) NOT NULL,
port INT NOT NULL,
database_name VARCHAR(128),
username VARCHAR(128),
-- 密码/密钥加密存储,绝不存明文
encrypted_cred TEXT,
connection_props JSONB, -- 额外参数,如 SSL、超时等
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
3.3 表(Table)模型
表是元数据模型的核心。你想想看,用户查数据目录,90% 的场景都是在找表。所以表的模型设计,直接影响用户体验。
3.3.1 核心属性
| 属性名 | 类型 | 说明 |
|---|---|---|
| table_id | UUID | 全局唯一标识 |
| datasource_id | UUID | 所属数据源 |
| schema_name | string | 数据库 schema,如 public、ods |
| table_name | string | 物理表名 |
| display_name | string | 业务展示名,如"订单主表" |
| table_type | enum | TABLE / VIEW / MATERIALIZED_VIEW |
| row_count | bigint | 行数(定期采样) |
| size_bytes | bigint | 存储大小 |
| description | text | 业务描述 |
| tags | string[] | 标签,如 PII、敏感、核心 |
这里有个细节:display_name 和 table_name 分开。为什么?因为物理表名往往是 ods_order_info_di 这种机器命名,用户根本看不懂。给一个业务展示名,搜索体验会好很多。
3.3.2 分区与生命周期
对于 Hive 表或大表,分区信息是必须的。我建议用 JSONB 字段存储分区定义,灵活且易于扩展。
-- 分区信息示例(JSONB)
{
"partition_keys": ["dt", "hour"],
"partition_type": "RANGE",
"retention_days": 30,
"latest_partition": "2025-03-15"
}
3.4 字段(Column)模型
字段是元数据最细粒度的实体。用户查字段,通常是为了确认某个字段的含义、类型、是否敏感。所以字段模型要兼顾技术属性和业务属性。
3.4.1 核心属性
| 属性名 | 类型 | 说明 |
|---|---|---|
| column_id | UUID | 全局唯一标识 |
| table_id | UUID | 所属表 |
| column_name | string | 物理字段名 |
| display_name | string | 业务展示名,如"用户ID" |
| data_type | string | 原始类型,如 bigint、varchar(256) |
| is_nullable | boolean | 是否可空 |
| default_value | string | 默认值 |
| ordinal_position | int | 字段顺序 |
| description | text | 业务含义描述 |
| is_sensitive | boolean | 是否敏感字段(PII) |
| sensitive_type | enum | 手机号 / 身份证 / 银行卡 / 地址 |
3.4.2 枚举值与字典映射
很多字段是枚举值,比如 status 字段有 0、1、2。用户查目录时,看到 0 根本不知道什么意思。我建议加一个 enum_values 字段,用 JSONB 存储映射关系。
-- 枚举值映射示例
{
"0": "待支付",
"1": "已支付",
"2": "已取消",
"3": "已退款"
}
这样前端展示时,直接渲染成中文标签,用户体验会好很多。
3.5 ETL任务(ETLJob)模型
ETL任务模型是数据血缘的基础。没有它,你根本不知道这张表的数据从哪来、到哪去。
3.5.1 核心属性
| 属性名 | 类型 | 说明 |
|---|---|---|
| job_id | UUID | 全局唯一标识 |
| job_name | string | 任务名称 |
| job_type | enum | Spark / Flink / SQL / Shell |
| schedule_expression | string | 调度表达式,如 0 0 2 * * ? |
| owner | string | 负责人 |
| status | enum | running / paused / failed / deprecated |
| last_run_time | timestamp | 最近一次运行时间 |
| last_run_status | enum | success / failed / running |
| description | text | 任务描述 |
3.5.2 输入输出血缘
ETL任务的血缘关系,我建议用单独的血缘表来维护。为什么?因为血缘关系是多对多的——一个任务可以读多张表、写多张表,一张表也可以被多个任务读写。
-- 血缘关系表
CREATE TABLE data_lineage (
lineage_id UUID PRIMARY KEY,
job_id UUID NOT NULL REFERENCES etl_job(job_id),
table_id UUID NOT NULL REFERENCES table_meta(table_id),
column_id UUID, -- 可选,字段级血缘
direction ENUM('INPUT', 'OUTPUT') NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- 查询示例:找某张表的上游任务
SELECT j.job_name, l.direction
FROM data_lineage l
JOIN etl_job j ON l.job_id = j.job_id
WHERE l.table_id = 'xxx' AND l.direction = 'OUTPUT';
3.6 关系设计策略
实体之间的关系,我总结为三种:层级关系、引用关系、血缘关系。
3.6.1 层级关系
数据源 → 表 → 字段,这是天然的 1:N 层级。设计时注意:
- 外键约束:表的外键指向数据源,字段的外键指向表。删除数据源时,级联删除所有表和字段。
- 冗余存储:我习惯在字段表里冗余一个 datasource_id,方便跨数据源搜索字段。虽然违反第三范式,但查询性能提升明显。
3.6.2 引用关系
ETL任务引用表,表引用数据源。这种引用关系不要用外键强约束,因为 ETL 任务可能引用一个已废弃的表。用软引用,查询时通过 JOIN 获取完整信息。
3.6.3 血缘关系
血缘关系是动态的,随着 ETL 任务的运行不断更新。我建议:
- 血缘数据单独存储,不要和实体混在一起。
- 每次 ETL 任务运行成功后,更新血缘关系。
- 保留历史血缘,方便回溯数据问题。
3.7 模型设计的常见坑
最后,分享几个我踩过的坑:
- 过度设计:一开始就想把字段级血缘、数据质量、生命周期全做进去。结果模型太复杂,半年都没上线。建议先做核心实体,再逐步扩展。
- 忽略版本:表结构会变,字段会增减。没有版本管理,历史数据全乱套。我建议加一个
version字段,每次变更生成新版本。 - 不做冗余:为了追求范式,查询时 JOIN 七八张表。性能扛不住。适当冗余,比如在字段表里冗余表名和数据源名,查询时少 JOIN 一次。
- 不设软删除:直接 DELETE 数据,血缘关系全断了。用 status 字段标记 deprecated,保留历史数据。
嗯,模型设计就聊到这里。记住一句话:模型是服务于业务的,不是用来炫技的。把核心实体定义清楚,关系理顺畅,后面的服务化改造才能站得住脚。