跳转至

血缘

语义模型把"revenue 是什么"回答一次;血缘图展示这个答案从哪里来。Dosi 把编译后的模型 投影成四条泳道的图:

泳道 节点 进入它的边
物理表 main.orders
数据集 orders reads_table
原子指标 revenue 从它读取的每个数据集来的 aggregates
派生指标 net_revenue 从它所依赖指标来的 derive_basecompose_memberwindow_basewindow_second

数据集之间由 join 边相连,每个 relationship 一条。图是模型的纯函数:同一个模型永远产出 相同的字节,没有时间戳,也不包含产出它的机器的任何信息。

获取

$ dosi lineage dump --model model.yaml > graph.json
$ dosi lineage view --model model.yaml            # 在浏览器里打开一个页面
from dosi_engine import Engine
graph = Engine(model_path="model.yaml").lineage()
graph = Engine(model_path="model.yaml").lineage(redact_sql=True)

view 生成一个自包含的 HTML 文件——不需要网络,通过 file:// 打开——泳道从左到右排列, 节点上带徽标,边按类型着色,有按名字过滤的输入框,还有一个面板显示节点的详情并可沿着边 点击跳转。派生指标那条泳道会按派生链的深度拆成多列:一个派生指标坐在它所有上游指标里最 靠右那个的右侧一列,这样一条 compose 链是从左往右读的,而不是挤成一列自己连自己。没有 浏览器时(--no-open,或者没有显示器的服务器)它改为打印文件路径。

redact_sql 保持图的形状,把每段 SQL 文本——数据集 source、字段表达式、指标表达式、 derive 谓词——以及每个不透明的扩展负载替换成 "<redacted>"。名字永远不会被脱敏。

节点携带什么

每个节点有 id(前缀 table:dataset:metric:)、namelabelbadges, 以及一个形状随类型而异的 detail 块。

  • 数据集:source(表名,或查询及其美化后的形式)、它读取的表、键、主时间维度,以及每个 字段的表达式、背后的物理列和它的 label
  • 指标:作者写的表达式及其方言、它读取的数据集和物理列、值得用来分组的维度、时间维度、参数、 window 和 derive 声明,以及它的语义——形状、归因策略、可加性。

dimensions 就是 dosi list dimensions --metric 推荐的那一份,顺序也一致: 主键、外键,以及被某个指标聚合的列都不在其中——一张把所有可达字段都列出来的图,等于把筛选工作 丢回给读者。没有列出的名字仍然可以分组:这个判定只是不推荐,从不阻止显式 group by。

label 是展示名。Apache Ossie 只给字段定义了展示名(字段上的 label,Databricks、dbt 等 converter 读作 display name 的正是这个属性),数据集和指标上还没有,所以数据集或指标的 label 总是等于它的 name。Dosi 从不自行发明标题:label == name 的意思是没人给过。

加载 ontology 之后

加载一份 Apache Ossie ontology,图就多出一个 ontology 块: 每个实体一个节点,以及把概念与上下泳道连起来的边。

$ dosi lineage view --ontology flights.yaml
从 → 到 含义
mapping 数据集 → 概念 实体映射到哪个数据集——它由之标识的那个,或者没有自己的表时,属性被反规范化到的那些数据集
relationship 概念 → 概念 两个实体之间一条可导航的关系,带 role 和 multiplicity;realized_by 列出它走过的 join

这两种边都是血缘:概念的数据在哪里。而 ontology 的 derived_by 规则对一个指标的描述属于 语义,所以它挂在指标节点上——v2 本来就把 dimensionstime_dimension 放在那里:

"concept": {
  "path": "Airport.average_departure_delay",   // 概念拼写
  "measure": "Flight.departure_delay",         // 聚合的值
  "group_by": "Airport",                       // 粒度
  "via": ["relationship:Flight.departs_from"], // 把两者关联起来的路径
  "realized_by": ["join:flight_departs_from"]
}

via 就是这层叠加存在的理由。一份 ontology 可以派生出两个 SQL 逐字节相同的指标——机场的 平均离港延误,和到达该机场的航班的平均离港延误——它们只在"哪条关系把航班关联到机场"上 不同。在平面图里两者无法区分;在这里各自写明自己的路径。

path 就是 describe 打印的、queryselect 接受的名字,所以图和查询工具说的是同一套 名字。模型里手写的指标没有 concept:它们的血缘是 aggregates 边,而那条边起点的数据集上 有指向其实体的 mapping 边。

概念节点说明自己的映射落在了什么上:table(有自己的数据集,一行一个对象)、 denormalized(没有自己的表;列在另一个实体的表上,那些行是那个实体的,它只是投影)、 unmapped(声明了,但什么都解析不到)。查看器把后两种画成虚线框。

叠加层不含 SQL,所以 redact_sql 不会改动它。没有 ontology 的模型根本没有 ontology 键——负载和 ontology 出现之前完全一样。

Python

Engine 接受 ontology 的两个方向与 CLI 相同:

Engine(ontology_path="flights.yaml")                       # 内嵌的模型就是模型
Engine(model_path="model.yaml", ontology_path="flights.yaml")  # 模型优先;ontology 提供概念
Engine(ontology_text=yaml_text, mapping="prod_mapping")     # 文本形式,并从多个 ontology_mappings 里选一个

之后 lineage() 就包含叠加层。ontology 声明了但 Dosi 无法降低的构造(n 元关系、未映射的概念) 在构造时以 RuntimeWarning 报告,与编译告警同一通道。 完全无法解析或降低的 ontology 会抛出 code 为 ontologyModelError;ontology 相关参数只能以关键字传入。

另见