字段级血缘模型:输入字段、输出字段、转换逻辑、依赖关系的数据模型设计

做数据血缘这么久,我最大的体会是:没有好的数据模型,血缘就是一团乱麻。你想想看,字段级血缘要记录的不只是「A表→B表」这种粗粒度关系,而是要精确到「A表的user_id字段经过什么逻辑变成了B表的customer_no字段」。这个粒度,对数据模型的要求就很高了。

我个人习惯把字段级血缘模型拆成四个核心要素:输入字段、输出字段、转换逻辑、依赖关系。这四个东西搞清楚了,血缘的骨架就搭起来了。

核心实体:字段节点

先看最基础的东西——字段节点。每个字段在血缘图中都是一个独立节点。我见过不少团队把字段挂在表下面当属性,结果查询某个字段的血缘时,得先查表再查字段,性能惨不忍睹。

我的做法是:字段独立成实体。每个字段有一个全局唯一ID,不管它属于哪张表、哪个视图、哪个文件。

字段节点核心属性

  • field_id:全局唯一标识,建议用UUID或雪花ID
  • field_name:字段名,如 user_id
  • table_id:所属表/视图/文件的ID
  • data_type:数据类型,如 string、int、decimal(18,2)
  • is_primary_key:是否主键字段
  • is_nullable:是否允许为空
  • description:字段业务含义

嗯,这里要注意:table_id 不能作为字段的唯一标识。为什么?因为同一个字段可能出现在多个表中(比如源表和目标表都有 user_id),但它们在血缘图中的身份是不同的节点。

转换逻辑:血缘的「灵魂」

字段节点只是静态信息,真正让血缘「活」起来的是转换逻辑。我在项目中遇到过最头疼的事:只知道字段A→字段B,但不知道中间经历了什么。排查数据问题时,你根本没法判断是源数据问题还是ETL逻辑问题。

所以,转换逻辑必须显式建模。我通常把它设计成一个独立实体,关联输入字段和输出字段。

属性 说明 示例
transform_id 转换唯一标识 TRANS_20240301_001
transform_type 转换类型 direct_map / expression / lookup / aggregation
transform_expression 转换表达式 CONCAT(first_name, ' ', last_name)
transform_code 转换代码片段 full_name = f"{first_name} {last_name}"
transform_desc 转换描述 将姓和名拼接成全名

我的经验:transform_expression 尽量用标准化的表达式语言(如SQL表达式),不要用自然语言描述。自然语言太模糊,机器没法解析。我曾经接手过一个项目,转换逻辑全是中文描述,比如「把金额除以10000转成万元」,结果做自动化影响分析时完全没法用。

依赖关系:有向无环图的核心

字段级血缘本质上是一个有向无环图(DAG)。每个节点是字段,每条边是依赖关系。边的方向从输入指向输出,表示「输出字段依赖于输入字段」。

依赖关系实体需要记录什么?

  • source_field_id:源字段(输入)
  • target_field_id:目标字段(输出)
  • transform_id:关联的转换逻辑
  • dependency_type:依赖类型(直接/间接/条件)
  • lineage_level:血缘层级(1表示直接依赖,2表示间接依赖)
  • created_at:血缘记录创建时间

你可能会问:为什么还要记录 lineage_level?说白了,这是为了性能优化。查询全链路血缘时,如果每次都递归遍历,数据量大了根本扛不住。预计算层级,可以大幅提升查询效率。

避坑指南:我曾经在一个项目中没做层级预计算,结果血缘图只有5000个节点,但递归查询一次要30秒。后来加了 lineage_level 字段并建了索引,查询时间降到了200毫秒。嗯,这个坑我替你们踩过了。

完整数据模型设计

把上面几个实体串起来,完整的字段级血缘数据模型大概是这样的:

-- 字段节点表
CREATE TABLE field_nodes (
    field_id       VARCHAR(64) PRIMARY KEY,
    field_name     VARCHAR(255) NOT NULL,
    table_id       VARCHAR(64) NOT NULL,
    data_type      VARCHAR(64),
    is_primary_key BOOLEAN DEFAULT FALSE,
    is_nullable    BOOLEAN DEFAULT TRUE,
    description    TEXT,
    created_at     TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at     TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 转换逻辑表
CREATE TABLE transform_logics (
    transform_id        VARCHAR(64) PRIMARY KEY,
    transform_type      VARCHAR(32) NOT NULL,
    transform_expression TEXT,
    transform_code      TEXT,
    transform_desc      TEXT,
    created_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 血缘依赖关系表
CREATE TABLE lineage_edges (
    edge_id          VARCHAR(64) PRIMARY KEY,
    source_field_id  VARCHAR(64) NOT NULL,
    target_field_id  VARCHAR(64) NOT NULL,
    transform_id     VARCHAR(64),
    dependency_type  VARCHAR(16) DEFAULT 'direct',
    lineage_level    INT DEFAULT 1,
    created_at       TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (source_field_id) REFERENCES field_nodes(field_id),
    FOREIGN KEY (target_field_id) REFERENCES field_nodes(field_id),
    FOREIGN KEY (transform_id) REFERENCES transform_logics(transform_id)
);

-- 索引:加速血缘查询
CREATE INDEX idx_lineage_source ON lineage_edges(source_field_id);
CREATE INDEX idx_lineage_target ON lineage_edges(target_field_id);
CREATE INDEX idx_lineage_level ON lineage_edges(lineage_level);

这个模型看起来简单,但实际落地时有很多细节。比如 transform_type 怎么分类?我一般分这几类:

  • direct_map:直接映射,字段名变了但值不变
  • expression:表达式转换,如 CONCAT、SUBSTR
  • lookup:查表转换,如通过字典表映射
  • aggregation:聚合转换,如 SUM、COUNT
  • conditional:条件转换,如 CASE WHEN
  • custom:自定义函数转换

分类越细,后续做影响分析时就越精准。比如你想知道「修改了某个字段的映射逻辑会影响哪些下游」,如果只记录了「expression」类型,你根本不知道具体改了啥。

用一张图看懂整体结构

说了这么多,不如直接画出来。下面这张图展示了字段级血缘模型的核心结构:

字段级血缘数据模型结构图 字段级血缘数据模型核心结构 输入字段 field_id field_name table_id / data_type 转换逻辑 transform_id transform_type transform_expression 输出字段 field_id field_name table_id / data_type 输入 输出 血缘依赖关系表 (lineage_edges) source_field_id → target_field_id transform_id (关联转换逻辑) dependency_type / lineage_level 💡 核心设计原则 1. 字段独立成节点,不依附于表 2. 转换逻辑显式建模,不隐含在代码中 3. 依赖关系预计算层级,避免递归查询

模型落地的几个关键点

模型设计好了,落地时还有几个坑要注意:

  1. 字段ID的生成策略:我建议用 table_id + field_name 的哈希值作为字段ID,这样同一个字段在不同环境(开发/测试/生产)中ID一致,方便做跨环境血缘对比。
  2. 转换逻辑的版本管理:ETL脚本会变,转换逻辑也会变。我习惯给 transform_logics 表加一个 version 字段,每次变更生成新版本,旧版本保留。这样你可以回溯「三个月前这个字段是怎么算出来的」。
  3. 血缘的时效性:血缘关系不是一成不变的。表结构改了、ETL逻辑改了,血缘都要跟着变。我建议加一个 valid_from 和 valid_to 时间窗口,支持血缘的快照查询。

一个小技巧:如果你用的是关系型数据库存储血缘,lineage_edges 表的数据量会增长很快。我建议按时间分区,比如按月分区。查询时带上时间条件,性能会好很多。