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) | 弱(最终一致性为主) | 强(以关系库为准) |
| 运维复杂度 | 低 | 高 | 中(多组件运维) |
| 适合元数据规模 | 百万级 | 千万级 | 亿级 |
| 典型场景 | 数据目录、字段详情 | 血缘分析、影响分析 | 企业级元数据平台 |
嗯,这张表我建议你保存下来。每次做技术选型时拿出来对照一下,能少走不少弯路。
我曾经在一个项目中,一开始选了纯关系型存储,结果血缘查询慢到被业务方投诉。后来改成混合架构,花了三周做数据迁移。那三周,我每天都在想——要是当初设计时就考虑好查询模式,该多好。