4. 元数据存储与建模

元数据存哪儿?怎么存?这个问题,我几乎在每个客户现场都会被问到。说实话,没有银弹。你想想看,元数据本身就有多种形态——技术元数据是表格结构,血缘关系是网状结构,业务术语又是层级结构。用一种存储打天下,往往两头不讨好。

这一章,我就把几种主流方案掰开揉碎讲清楚。包括我踩过的坑,和一些事后看来「早知道就好了」的设计原则。

4.1 关系型存储:最稳妥的选择

关系型数据库(MySQL、PostgreSQL)是元数据管理的「老黄牛」。我最早做元数据平台时,用的就是 MySQL。为什么?因为团队对关系型数据库最熟,运维成本最低。

关系型存储的核心思路是:把元数据实体映射成表,实体间的关系映射成外键或关联表。

举个例子,一个数据表的元数据模型大概长这样:

-- 表元数据
CREATE TABLE metadata_table (
    id BIGINT PRIMARY KEY,
    database_name VARCHAR(128),
    schema_name VARCHAR(128),
    table_name VARCHAR(128),
    table_type VARCHAR(32),  -- TABLE / VIEW
    create_time TIMESTAMP,
    update_time TIMESTAMP,
    UNIQUE KEY uk_table (database_name, schema_name, table_name)
);

-- 字段元数据
CREATE TABLE metadata_column (
    id BIGINT PRIMARY KEY,
    table_id BIGINT NOT NULL,
    column_name VARCHAR(128),
    data_type VARCHAR(64),
    comment TEXT,
    ordinal_position INT,
    is_nullable BOOLEAN,
    FOREIGN KEY (table_id) REFERENCES metadata_table(id)
);

-- 表与表之间的血缘关系
CREATE TABLE metadata_lineage (
    id BIGINT PRIMARY KEY,
    source_table_id BIGINT NOT NULL,
    target_table_id BIGINT NOT NULL,
    source_column_id BIGINT,
    target_column_id BIGINT,
    transform_expr TEXT,
    FOREIGN KEY (source_table_id) REFERENCES metadata_table(id),
    FOREIGN KEY (target_table_id) REFERENCES metadata_table(id)
);

这个模型清晰吧?但问题也明显——血缘关系的深度查询非常痛苦。你想查一张表的完整上游链路,得写递归 CTE,性能还差。我曾经在一个 MySQL 实例上跑 5 层血缘查询,耗时超过 10 秒。嗯,这显然不能用于实时展示。

适用场景:元数据量在百万级以内、查询模式固定、团队对关系型数据库运维经验丰富。

4.2 图数据库存储:血缘查询的利器

为什么图数据库适合存元数据?说白了,元数据本身就是一张大图。表是节点,字段是节点,ETL 任务是节点,它们之间的依赖、派生、映射关系就是边。

我选择 Neo4j 做过一个实验:同样查 5 层血缘,MySQL 要 10 秒,Neo4j 只要 50 毫秒。差距就是这么夸张。

图模型的设计思路是:节点代表实体,关系代表语义连接。

// 创建节点
CREATE (t:Table {name: 'order_detail', database: 'dw', schema: 'dwd'})
CREATE (c:Column {name: 'order_amount', data_type: 'decimal(18,2)'})
CREATE (etl:ETLJob {name: 'job_order_summary', cron: '0 0 3 * *'})

// 创建关系
CREATE (t)-[:HAS_COLUMN]->(c)
CREATE (etl)-[:WRITES_TO]->(t)
CREATE (t)-[:DEPENDS_ON]->(:Table {name: 'order_raw'})

查询上游血缘,一行 Cypher 搞定:

MATCH (t:Table {name: 'order_detail'})<-[:DEPENDS_ON*1..5]-(upstream)
RETURN upstream

是不是很直观?但图数据库也有短板:事务能力弱,批量导入慢,运维门槛高。我见过一个团队把全量元数据(几千万节点)往 Neo4j 里灌,跑了整整两天没跑完。最后不得不改成增量同步。

注意:图数据库不适合存「大字段」元数据,比如字段的完整描述文档、采样数据分布。这些信息建议放在关系型或文档型数据库中。

4.3 混合存储架构:取长补短

既然关系型和图数据库各有优劣,那能不能把它们结合起来?当然可以。这也是我目前在多个生产项目中采用的方案。

混合架构的核心思路是:用关系型数据库存「元数据的事实」,用图数据库存「元数据的关系」。

具体来说:

  • 关系型数据库:存表、字段、分区、任务的详细属性。这些数据是结构化的,需要强一致性,支持复杂过滤和分页。
  • 图数据库:存血缘、影响分析、数据链路。这些数据是关系密集型的,需要快速遍历。
  • 搜索引擎(Elasticsearch):存全文索引,支持模糊搜索和标签检索。我习惯把字段注释、业务描述、样例数据都扔进去。

下面这张图展示了混合架构的数据流转:

混合存储架构数据流转 元数据采集器 关系型存储 表/字段/分区详情 图数据库 血缘/影响分析 搜索引擎 全文检索/标签 统一元数据查询服务 数据目录 / 数据血缘 / 影响分析

这个架构的好处是:查询血缘走图库,查询详情走关系库,搜索走 ES。各司其职,互不干扰。我在一个日增量 50 万条元数据的项目中用这套方案,查询 P99 延迟控制在 200ms 以内。

实践经验:混合架构的关键是「数据同步一致性」。我建议用 CDC(变更数据捕获)机制,采集器先写关系库,然后通过消息队列异步同步到图库和 ES。这样即使图库挂了,元数据也不会丢。

4.4 元数据模型设计原则

模型设计得好不好,直接决定了后续查询的效率和扩展的灵活性。我总结了 5 条原则,每条都是用教训换来的。

原则一:实体与关系分离

不要把关系编码到实体属性里。比如,不要在表记录里写一个 parent_table_id 字段来表示血缘。这样做,查询时只能逐层递归,性能极差。正确的做法是:实体只描述自身,关系用单独的边表或图关系来表达。

原则二:版本化设计

元数据是会变的。表结构会改,字段会新增,ETL 逻辑会调整。如果不做版本化,历史血缘就查不到了。

我习惯在每个元数据实体上加两个字段:valid_from 和 valid_to。查询时默认只查当前版本,需要追溯历史时再查全量。

CREATE TABLE metadata_table_version (
    id BIGINT PRIMARY KEY,
    table_id BIGINT NOT NULL,        -- 逻辑主键
    database_name VARCHAR(128),
    table_name VARCHAR(128),
    schema_json TEXT,                 -- 全量快照
    valid_from TIMESTAMP NOT NULL,
    valid_to TIMESTAMP,              -- NULL 表示当前版本
    version INT NOT NULL
);

原则三:最小冗余原则

同一个元数据信息,只在一个地方存。比如字段的数据类型,在关系库里存一份就够了,不要为了查询方便也塞到图库的节点属性里。冗余一时爽,数据不一致时火葬场。

原则四:可扩展的标签体系

元数据的属性是动态增长的。今天加个「数据等级」,明天加个「合规标签」。如果每次加属性都改表结构,运维成本太高。

我推荐用 KV 扩展属性表 或 标签系统 来解决:

CREATE TABLE metadata_tag (
    id BIGINT PRIMARY KEY,
    entity_type VARCHAR(64),   -- TABLE / COLUMN / JOB
    entity_id BIGINT,
    tag_key VARCHAR(128),
    tag_value TEXT,
    UNIQUE KEY uk_tag (entity_type, entity_id, tag_key)
);

原则五:查询模式驱动模型

不要先建模型再想查询。反过来,先列出所有查询场景,再设计模型。

举个例子,如果你的核心查询是「给定一张表,找出所有上游表和下游表」,那图模型就是首选。如果你的核心查询是「搜索字段名包含 'amount' 的所有表」,那 ES 索引就是关键。

核心观点:没有完美的元数据模型,只有适合你查询场景的模型。混合架构的本质,就是用多种模型去适配多种查询。

4.5 选型对比一览

最后,我把三种存储方案的对比整理成一张表,方便你快速决策:

维度 关系型存储 图数据库存储 混合存储
查询血缘深度 差(递归 CTE,3 层以上性能骤降) 优(毫秒级遍历任意深度) 优(图库负责血缘)
复杂过滤/聚合 优(SQL 支持完善) 一般(Cypher/Gremlin 表达能力有限) 优(关系库负责过滤)
事务与一致性 强(ACID) 弱(最终一致性为主) 强(以关系库为准)
运维复杂度 低 高 中(多组件运维)
适合元数据规模 百万级 千万级 亿级
典型场景 数据目录、字段详情 血缘分析、影响分析 企业级元数据平台

嗯,这张表我建议你保存下来。每次做技术选型时拿出来对照一下,能少走不少弯路。

我曾经在一个项目中,一开始选了纯关系型存储,结果血缘查询慢到被业务方投诉。后来改成混合架构,花了三周做数据迁移。那三周,我每天都在想——要是当初设计时就考虑好查询模式,该多好。