3. 元数据模型设计:核心实体的定义与关系策略

元数据模型,说白了就是数据目录的骨架。骨架搭歪了,后面再怎么填肉都别扭。我在好几个项目里都见过这种场景——模型设计时图省事,把所有东西塞进一张大表,结果查询性能崩了,关系也理不清。

这一章,我们就来聊聊数据目录里最核心的四个实体:数据源、表、字段、ETL任务。以及它们之间该怎么关联,属性该怎么设计。

3.1 核心实体概览

先看一张整体结构图,帮你快速建立认知。

数据目录核心实体关系图 数据源 (DataSource) MySQL / Hive / Kafka 表 (Table) 物理表 / 视图 / 逻辑表 字段 (Column) 名称 / 类型 / 描述 ETL任务 (ETLJob) 调度 / 血缘 / 依赖 1 : N 1 : N N : M 核心原则:上层实体独立存在,下层实体通过外键关联上层,ETL任务与表/字段通过血缘关系连接

嗯,这张图很直观。从上到下是数据源 → 表 → 字段的层级关系,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
小技巧:status 字段别只搞 active/deleted,加一个 deprecated 状态。我曾经遇到一个场景:数据源下线了,但历史血缘还要保留。用 deprecated 标记,既不影响查询,又不会误删。

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()
);
注意:密码字段必须加密。我曾经接手过一个项目,密码直接明文存在数据库里,审计时被安全团队直接打回。建议用 AES-256 或集成密钥管理服务(KMS)。

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"
}
避坑指南:我曾经遇到一个项目,把分区字段硬编码成单独列,结果 Hive 表改了分区策略,代码全要重写。用 JSONB 存储,兼容性会好很多。

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 手机号 / 身份证 / 银行卡 / 地址
经验之谈:is_sensitive 和 sensitive_type 一定要分开。为什么?因为敏感类型决定了脱敏策略。手机号脱敏是 138****1234,身份证是 110***********1234,算法不一样。

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';
注意:字段级血缘(column_id)是可选的。我建议先做表级血缘,再做字段级。因为字段级血缘的解析成本很高,而且很多 ETL 工具不支持。先跑通表级,再逐步细化。

3.6 关系设计策略

实体之间的关系,我总结为三种:层级关系、引用关系、血缘关系。

3.6.1 层级关系

数据源 → 表 → 字段,这是天然的 1:N 层级。设计时注意:

  • 外键约束:表的外键指向数据源,字段的外键指向表。删除数据源时,级联删除所有表和字段。
  • 冗余存储:我习惯在字段表里冗余一个 datasource_id,方便跨数据源搜索字段。虽然违反第三范式,但查询性能提升明显。

3.6.2 引用关系

ETL任务引用表,表引用数据源。这种引用关系不要用外键强约束,因为 ETL 任务可能引用一个已废弃的表。用软引用,查询时通过 JOIN 获取完整信息。

3.6.3 血缘关系

血缘关系是动态的,随着 ETL 任务的运行不断更新。我建议:

  • 血缘数据单独存储,不要和实体混在一起。
  • 每次 ETL 任务运行成功后,更新血缘关系。
  • 保留历史血缘,方便回溯数据问题。
核心原则:实体模型要稳定,血缘模型要灵活。实体是骨架,血缘是血管。骨架不能轻易动,血管可以随时调整。

3.7 模型设计的常见坑

最后,分享几个我踩过的坑:

  1. 过度设计:一开始就想把字段级血缘、数据质量、生命周期全做进去。结果模型太复杂,半年都没上线。建议先做核心实体,再逐步扩展。
  2. 忽略版本:表结构会变,字段会增减。没有版本管理,历史数据全乱套。我建议加一个 version 字段,每次变更生成新版本。
  3. 不做冗余:为了追求范式,查询时 JOIN 七八张表。性能扛不住。适当冗余,比如在字段表里冗余表名和数据源名,查询时少 JOIN 一次。
  4. 不设软删除:直接 DELETE 数据,血缘关系全断了。用 status 字段标记 deprecated,保留历史数据。

嗯,模型设计就聊到这里。记住一句话:模型是服务于业务的,不是用来炫技的。把核心实体定义清楚,关系理顺畅,后面的服务化改造才能站得住脚。