数据血缘的存储模型:从表级到字段级,你该怎么选?

聊到数据血缘,很多人第一反应是「画个图嘛」。但真正落地的时候你会发现——怎么存、存多细、用什么存,这三个问题能卡住你很久。

我个人习惯把血缘存储拆成四个层次来看:表级血缘、字段级血缘、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
运维成本 低 高 中 高
适用阶段 初期 成熟期 全周期 扩展期

一张图看懂血缘存储体系

下面这张图是我自己梳理的,把四种存储模型的关系画了出来。你看完应该能明白它们各自的位置。

数据血缘存储模型体系 数据源 / 任务 ETL血缘(任务级) 表级血缘 字段级血缘 关系型数据库(MySQL/PG) 图数据库(Neo4j) 核心原则 1. 先表级后字段级,不要一步到位 2. ETL血缘是连接「任务」和「数据」的桥梁 3. 关系型适合单点查询,图数据库适合多跳追溯

怎么选?我给你三个判断标准

说了这么多,你可能还是纠结。我直接给结论:

  1. 团队规模 < 10 人,数据量 < 100 张表:关系型数据库 + 表级血缘就够了。别折腾 Neo4j,你玩不转。
  2. 团队 10-30 人,数据量 100-1000 张表:关系型存表级和 ETL 血缘,Neo4j 只存字段级。两个系统之间通过 Job ID 关联。
  3. 团队 > 30 人,数据量 > 1000 张表:全量上 Neo4j,但要做好元数据治理。我曾经见过一个团队,Neo4j 里塞了 500 万个节点,查询慢到怀疑人生——后来发现是没做索引和节点合并。

避坑指南:不管你用哪种存储,一定要给血缘数据加上时间戳和版本号。我曾经因为没有版本号,回滚数据后血缘图全乱了——上游指向了旧表,下游还在用新表。那一次排查花了整整两天。