字段级血缘模型:输入字段、输出字段、转换逻辑、依赖关系的数据模型设计
做数据血缘这么久,我最大的体会是:没有好的数据模型,血缘就是一团乱麻。你想想看,字段级血缘要记录的不只是「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」类型,你根本不知道具体改了啥。
用一张图看懂整体结构
说了这么多,不如直接画出来。下面这张图展示了字段级血缘模型的核心结构:
模型落地的几个关键点
模型设计好了,落地时还有几个坑要注意:
- 字段ID的生成策略:我建议用
table_id + field_name的哈希值作为字段ID,这样同一个字段在不同环境(开发/测试/生产)中ID一致,方便做跨环境血缘对比。 - 转换逻辑的版本管理:ETL脚本会变,转换逻辑也会变。我习惯给
transform_logics表加一个version字段,每次变更生成新版本,旧版本保留。这样你可以回溯「三个月前这个字段是怎么算出来的」。 - 血缘的时效性:血缘关系不是一成不变的。表结构改了、ETL逻辑改了,血缘都要跟着变。我建议加一个
valid_from和valid_to时间窗口,支持血缘的快照查询。
一个小技巧:如果你用的是关系型数据库存储血缘,lineage_edges 表的数据量会增长很快。我建议按时间分区,比如按月分区。查询时带上时间条件,性能会好很多。