4. 元数据存储:选型与实战

元数据存哪儿?怎么存?这是做数据目录绕不开的核心问题。我见过不少团队,一上来就选了个时髦的存储,结果跑半年发现查询慢得离谱,或者模型改不动。今天咱们就把关系型数据库、图数据库(以 Neo4j 为例)掰开揉碎聊清楚,最后给出一套选型框架。

核心观点:没有银弹。元数据存储选型取决于你的查询模式、数据规模和团队维护成本。

4.1 关系型数据库存储

关系型数据库(MySQL、PostgreSQL)是元数据存储的“老黄牛”。我最早做数据治理项目时,就是用 PostgreSQL 来存表结构、字段描述和血缘关系。说实话,对于大部分中小规模场景,它完全够用。

4.1.1 为什么选关系型?

  • 成熟稳定:事务支持好,ACID 特性让你不用担心数据不一致。
  • 查询灵活:SQL 是通用语言,团队里随便拉个后端都能写。
  • 生态丰富:ORM 框架、迁移工具、备份方案一应俱全。

4.1.2 怎么设计表结构?

我习惯用“实体-属性-关系”的思路来建模。举个例子,存储表和字段的元数据:

-- 表信息
CREATE TABLE metadata_table (
    id BIGSERIAL PRIMARY KEY,
    database_name VARCHAR(100) NOT NULL,
    schema_name VARCHAR(100) NOT NULL,
    table_name VARCHAR(100) NOT NULL,
    table_comment TEXT,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(database_name, schema_name, table_name)
);

-- 字段信息
CREATE TABLE metadata_column (
    id BIGSERIAL PRIMARY KEY,
    table_id BIGINT REFERENCES metadata_table(id),
    column_name VARCHAR(100) NOT NULL,
    data_type VARCHAR(50) NOT NULL,
    is_nullable BOOLEAN DEFAULT TRUE,
    column_comment TEXT,
    ordinal_position INT NOT NULL
);

-- 血缘关系(简单版)
CREATE TABLE lineage_edge (
    id BIGSERIAL PRIMARY KEY,
    source_column_id BIGINT REFERENCES metadata_column(id),
    target_column_id BIGINT REFERENCES metadata_column(id),
    transformation_type VARCHAR(50),
    created_at TIMESTAMP DEFAULT NOW()
);

避坑指南:我曾经在血缘关系表上直接用递归 CTE 查多层依赖,结果数据量到 10 万条时查询直接超时。后来加了 path_length 字段和层级缓存才解决。

4.1.3 关系型数据库的痛点

  • 多对多关系查询复杂:比如“某个字段被哪些下游表引用”,需要多次 JOIN 或递归 CTE。
  • 深度血缘查询性能差:超过 5 层的血缘链路,SQL 写起来像天书,执行计划也难看。
  • 模型变更成本高:元数据模型经常要加新属性,ALTER TABLE 在线上环境是个麻烦事。

4.2 图数据库存储(Neo4j)

图数据库天生就是为关系而生的。我第一次用 Neo4j 做元数据存储时,感觉就像从骑自行车换成了开跑车——尤其是查血缘关系时,那种丝滑感让人上瘾。

4.2.1 为什么选图数据库?

  • 关系查询是原生能力:Cypher 查询语言让你用几行代码就能表达复杂的多跳关系。
  • 深度遍历性能优异:10 层血缘关系,Neo4j 可能比关系型快 100 倍。
  • 模型灵活:节点和关系可以随时加属性,不需要改 schema。

4.2.2 怎么建模?

在 Neo4j 中,我把表、字段、ETL 任务都建模为节点,把依赖关系建模为关系。举个例子:

// 创建节点
CREATE (t:Table {name: 'orders', database: 'dw', schema: 'public'})
CREATE (c:Column {name: 'order_id', data_type: 'bigint'})
CREATE (c2:Column {name: 'user_id', data_type: 'bigint'})

// 创建关系
CREATE (t)-[:HAS_COLUMN]->(c)
CREATE (t)-[:HAS_COLUMN]->(c2)

// 查询字段的血缘链路
MATCH (c:Column {name: 'order_id'})-[r:DERIVED_FROM*1..5]->(source:Column)
RETURN c, r, source

注意:Neo4j 的 *1..5 表示遍历 1 到 5 层关系。我曾经在生产环境没加深度限制,结果一个查询跑了 30 秒,把 CPU 打满了。建议默认限制在 10 层以内。

4.2.3 图数据库的痛点

  • 学习曲线陡:Cypher 语法和 SQL 差异大,团队需要时间适应。
  • 事务能力弱:Neo4j 的 ACID 支持不如关系型数据库,高并发写入时可能遇到问题。
  • 运维成本高:集群搭建、备份恢复、监控告警都比关系型复杂。

4.3 元数据存储选型对比

下面这张表是我在实际项目中总结的对比,你可以直接拿来当决策参考:

维度 关系型数据库 图数据库(Neo4j)
查询模式 点查询、简单关联 深度遍历、多跳关系
血缘查询性能 3 层以内尚可,超过 5 层吃力 10 层以内丝滑流畅
模型灵活性 需要提前设计 schema,变更成本高 无 schema 约束,随时加属性
事务支持 强 ACID 弱 ACID,适合读多写少
团队门槛 低,SQL 是通用技能 中高,需要学习 Cypher
运维复杂度 低,生态成熟 中高,集群管理复杂
典型场景 数据目录基础信息、字段字典 血缘分析、影响分析、数据地图

我的建议:如果元数据量在 100 万节点以内,且查询以“查某个表有哪些字段”为主,关系型数据库完全够用。如果要做全链路血缘分析、影响分析(比如“这个字段被改了会影响哪些报表”),图数据库是更好的选择。

4.4 核心逻辑:元数据存储选型决策流程

下面这张图是我自己画的一个决策流程,帮你快速判断该选哪种存储:

元数据存储选型决策流程 元数据存储需求 是否需要深度血缘分析? 是 否 图数据库(Neo4j) 关系型数据库 团队有 Cypher 经验? 数据量 > 100万节点? 选 Neo4j,先做 POC 选关系型,加缓存层 提示:选型不是一次性的,可以混合使用 关系型存基础信息,图数据库存血缘关系

4.5 混合存储策略

你可能会问:能不能两个都要?当然可以。我在一个大型数据平台项目中,就用了混合存储方案:

  • 关系型数据库(PostgreSQL):存储表、字段、ETL 任务的基础元数据,以及用户权限、标签等结构化信息。
  • 图数据库(Neo4j):存储血缘关系、影响链路、数据地图等需要深度遍历的数据。
  • 同步机制:通过 CDC(变更数据捕获)或定时任务,将关系型中的基础数据同步到图数据库中。

避坑指南:我曾经在混合存储中踩过一个坑——两边数据不一致。比如关系型里删了一张表,但图数据库里还留着它的血缘关系。后来我加了版本号和同步校验机制,每次同步都做全量对账,才彻底解决。

4.6 选型总结

说了这么多,最后给你一个简单的判断标准:

  • 场景一:你只需要查“这个表有哪些字段”“这个字段的类型是什么”——关系型数据库,别犹豫。
  • 场景二:你需要查“这个字段被哪些报表引用”“这个字段的完整血缘链路是什么”——图数据库,值得投入。
  • 场景三:你两个都要,而且数据量大、团队有资源——混合存储,但要做好数据同步和一致性保障。

嗯,选型这事儿没有标准答案。我见过用 MySQL 撑起 500 万节点元数据的团队,也见过用 Neo4j 只存了 1 万条血缘关系的团队。关键是想清楚你的核心查询模式,然后选一个你团队能 hold 住的方案。