数据血缘的存储模型:从表级到字段级,你该怎么选?
聊到数据血缘,很多人第一反应是「画个图嘛」。但真正落地的时候你会发现——怎么存、存多细、用什么存,这三个问题能卡住你很久。
我个人习惯把血缘存储拆成四个层次来看:表级血缘、字段级血缘、ETL血缘、以及最终的图存储。每一层解决的问题不一样,存储方式也天差地别。
表级血缘:最粗的粒度,最稳的起点
表级血缘,说白了就是「哪张表 -> 哪张表」。比如一张 ods_order 经过清洗变成了 dwd_order,再聚合到 dws_order_daily。这就是一条表级链路。
我在项目中遇到过不少团队,一上来就想搞字段级血缘,结果数据量太大、解析成本太高,最后连表级血缘都没跑通。我的建议是:先搞定表级,再往下钻。
表级血缘的存储模型很简单,关系型数据库就能搞定:
CREATE TABLE table_lineage (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
source_table VARCHAR(255) NOT NULL,
target_table VARCHAR(255) NOT NULL,
job_name VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_source (source_table),
INDEX idx_target (target_table)
);
嗯,就这么简单。一张表,两个字段,搞定上下游关系。你想想看,如果连这个都跑不出来,后面的字段级就更别想了。
字段级血缘:精细但烧钱
字段级血缘要回答的是「这个字段从哪来」。比如 dwd_order.user_id 是从 ods_order.user_id 直接映射过来的,还是经过了 COALESCE 或 CASE WHEN 处理?
这里有个坑——字段级血缘的存储量是表级的 N 倍。一张表如果有 50 个字段,上下游各 10 张表,那就是 500 条记录起步。我见过一个中型数仓,字段级血缘表跑了 3 个月,数据量超过 2 亿行。
存储模型上,我推荐用这种结构:
CREATE TABLE field_lineage (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
source_table VARCHAR(255),
source_field VARCHAR(255),
target_table VARCHAR(255),
target_field VARCHAR(255),
transform_type VARCHAR(50), -- direct, expression, function
transform_expr TEXT, -- 如果是表达式,存这里
job_name VARCHAR(255),
job_run_id VARCHAR(100),
created_at TIMESTAMP
);
注意:字段级血缘的 transform_expr 字段很容易被忽略。我曾经踩过这个坑——只存了映射关系,没存转换逻辑,结果半年后回溯数据质量问题时,根本不知道某个字段是经过 SUM 还是 AVG 聚合来的。后来不得不重跑所有历史任务去补这个信息。
ETL血缘:把「过程」也存下来
表级和字段级血缘存的是「结果」,而 ETL 血缘存的是「过程」。它记录的是:哪个 Job、在什么时间、用什么逻辑、把数据从 A 搬到了 B。
我个人习惯把 ETL 血缘和任务调度系统(比如 Dagster、Airflow)的元数据打通。这样当某个上游任务失败时,我能快速知道下游哪些表会受影响。
存储模型大概长这样:
CREATE TABLE etl_lineage (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
job_name VARCHAR(255),
job_type VARCHAR(50), -- batch, streaming, sql
source_system VARCHAR(255),
target_system VARCHAR(255),
source_objects JSON, -- 上游表/文件列表
target_objects JSON, -- 下游表/文件列表
execution_id VARCHAR(100),
start_time TIMESTAMP,
end_time TIMESTAMP,
status VARCHAR(20),
row_count BIGINT,
error_message TEXT
);
小技巧:ETL 血缘的 source_objects 和 target_objects 用 JSON 存,而不是拆成多行。这样查询时虽然要解析 JSON,但写入性能好很多。我一般在写入时用 JSON,查询时配合虚拟列或视图来加速。
血缘图的存储方式:Neo4j vs 关系型数据库
好,到了最核心的问题——这些血缘关系到底用什么存?
我两种都用过,说说我的感受。
关系型数据库(MySQL/PostgreSQL)
适合场景:表级血缘、中小规模(节点数 < 10万)。
优点:
- 团队熟悉,运维成本低
- 事务支持好,数据一致性有保障
- 配合索引,单点查询很快
缺点:
- 多跳查询(比如查「A 表所有下游的下游」)性能差,需要递归 CTE
- 字段级血缘的关联查询容易变成笛卡尔积
举个例子,查 A 表的所有下游:
WITH RECURSIVE downstream AS (
SELECT target_table FROM table_lineage WHERE source_table = 'A'
UNION ALL
SELECT tl.target_table FROM table_lineage tl
INNER JOIN downstream d ON tl.source_table = d.target_table
)
SELECT * FROM downstream;
嗯,能跑,但 5 层以上就开始慢了。
Neo4j 图数据库
适合场景:字段级血缘、大规模(节点数 > 10万)、需要频繁做多跳查询。
优点:
- 多跳查询是原生能力,性能好
- 模型直观——节点是表/字段,边是血缘关系
- 可视化方便,Neo4j Browser 直接出图
缺点:
- 团队需要学习 Cypher 查询语言
- 运维成本高,集群部署复杂
- 事务模型弱于关系型数据库
在 Neo4j 里,表级血缘的模型是这样的:
// 创建节点
CREATE (ods:Table {name: 'ods_order', schema: 'public'})
CREATE (dwd:Table {name: 'dwd_order', schema: 'dwd'})
// 创建关系
CREATE (ods)-[:FLOWS_TO {job: 'etl_order_v1'}]->(dwd)
字段级血缘就更直观了:
// 字段节点
CREATE (f1:Field {name: 'user_id', table: 'ods_order'})
CREATE (f2:Field {name: 'user_id', table: 'dwd_order'})
// 字段映射关系
CREATE (f1)-[:MAPS_TO {transform: 'direct'}]->(f2)
我的建议:如果你从零开始,先用关系型数据库把表级血缘跑通。等业务方开始问「这个字段到底从哪来的」的时候,再考虑上 Neo4j 做字段级。别一上来就上图数据库——我见过太多团队把 Neo4j 当 MySQL 用,最后查询没快多少,运维成本翻了三倍。
四种存储模型的对比
| 维度 | 表级血缘 | 字段级血缘 | ETL血缘 | 图存储 |
|---|---|---|---|---|
| 粒度 | 表 | 字段 | 任务/作业 | 节点+边 |
| 数据量 | 小(千级) | 大(百万级) | 中(万级) | 取决于节点数 |
| 查询复杂度 | 低 | 高 | 中 | 中(多跳快) |
| 推荐存储 | 关系型 | Neo4j | 关系型 | Neo4j |
| 运维成本 | 低 | 高 | 中 | 高 |
| 适用阶段 | 初期 | 成熟期 | 全周期 | 扩展期 |
一张图看懂血缘存储体系
下面这张图是我自己梳理的,把四种存储模型的关系画了出来。你看完应该能明白它们各自的位置。
怎么选?我给你三个判断标准
说了这么多,你可能还是纠结。我直接给结论:
- 团队规模 < 10 人,数据量 < 100 张表:关系型数据库 + 表级血缘就够了。别折腾 Neo4j,你玩不转。
- 团队 10-30 人,数据量 100-1000 张表:关系型存表级和 ETL 血缘,Neo4j 只存字段级。两个系统之间通过 Job ID 关联。
- 团队 > 30 人,数据量 > 1000 张表:全量上 Neo4j,但要做好元数据治理。我曾经见过一个团队,Neo4j 里塞了 500 万个节点,查询慢到怀疑人生——后来发现是没做索引和节点合并。
避坑指南:不管你用哪种存储,一定要给血缘数据加上时间戳和版本号。我曾经因为没有版本号,回滚数据后血缘图全乱了——上游指向了旧表,下游还在用新表。那一次排查花了整整两天。