SQLLineage v1.3:迈向字段血缘

字段级别的SQL血缘解析
February 20, 2022 · 10 min read

SQLLineage是一个基于Python的SQL血缘分析工具。2020年9月v1.0版本发布的时候, 写过一篇介绍的博客。 v1.0版本发布时,SQLLineage基本上达到甚至稍稍超越了我19年开始做这个项目时候的设想。此前在做离线任务调度系统的我,所希望的就是有一个库, 我把SQL告诉它,它可以回答我这段SQL读了什么表,最终又写入了哪张表。这样我可以灵活地去做SQL任务的依赖检查和推荐。

v1.0版本针各类SQL语句做了充分的测试,就表级血缘这一功能来讲,是生产环境高度可用的。而我甚至还做了基于DAG的血缘可视化。

之后的两个版本:

  • v1.1版本的迭代,将血缘可视化,由matplotlib+graphviz的服务端绘图方案,切换为基于Cytoscape.js的客户端绘图方案。交互性更强的同时,也摆脱了对graphviz的依赖。 Windows下安装C Library及其Python binding并不那么容易,之前经常有用户抱怨pygraphviz安装不上。移除之后,兼容性的问题也就同步解决了。

  • v1.2版本,随着React+MaterialUI的引入,SQLLineage成为了一个独立的Web App,我部署了一个DEMO站点,这样即使没有Python背景的人, 也可以使用SQLLineage了,进一步增强了易用性。(关于在Python Package里发布前端代码,感兴趣可以看这篇博客

就我个人的想法,做到这个阶段,是到了事了拂衣去的时候了。

然而社区里一直有做字段级别血缘的呼声,长期以来,Column-Level Lineage #103 都是讨论最热烈、回应最多的Issue。

“屈服”之前,我也想讲讲我的“苦衷”。

为什么不做

最直观的原因:做不准。

SQLLineage从设计上是一个静态SQL代码分析工具,只分析SQL文本,不会尝试去执行SQL。不执行,也不会查询元数据。我希望采用通用的SQL parser, 来支持尽可能多的SQL方言,这同时这也意味着如果要去对接每种SQL的元数据系统,作为一个个人项目,哪怕是社区驱动的开源项目,工作量都是无法想象的。

对于表级别血缘来说,静态分析几乎已经足够了。我们抽取出的表名,也还是本文形式返回。如果出现一张表在SQL语句中用到,但其实不存在的情况,作为工具, 我就原样返回,并不承诺它在元数据中存在。如果SQL里用到表时没有写库名的前缀,不知道来自哪个库。这也没关系,没有库名就是默认的库, 用户自然知道默认的库是哪个,我并不需要、也不可能比用户知道更多。

但对于字段级别血缘来说,类似的方式就不太友好。当多表关联的时候,一个字段没有写表名的前缀,它可能来自任何一张关联表。或者SELECT *的情况, 不查元数据,根本不可能知道到底SELECT了哪些字段。这种情形下,表级血缘可以绘制成精美的有向无环图,字段级血缘强行绘制的话,恐怕大部分点都是割裂的。 这对于用户来说,显然不够。

当时的想法是,也许有一天,会有一个类似Hive metastore这样的元数据系统,作为统一的标准出现,各大数据库/数据仓库厂商都支持这一标准。 或者哪怕没有数据库层面的标准,当Python社区出现了类似JDBC这样的标准接口(相比JDBC,Python社区的DB-API2显然做得还不够),那么我再来考虑字段血缘不迟。

然而社区在通过自己的形式告诉我,我们等不了。那我想,来都来了,就试着做做吧。就算最后不行,起码我可以说出《飞越疯人院》里的名言,"At least I tried".

设计原则

任何事情都不是空中楼阁,好也罢、坏也罢,之前的基础很大程度上决定之后的选择,所谓“路径依赖”。我考虑了一下,下面两个原则, 是至少在第一个版本的字段血缘中,我会选择继续坚持的:

  1. SQLLineage主要仍将会是一个静态代码分析工具。 这也意味着在做字段血缘时,我们需要容忍信息不准确。也许在未来的版本中, 我们可以提供一个插件的机制,由用户来注册元数据信息。通过这些元数据信息,我们来完善光凭静态分析得到的不准确的血缘。 但无论如何,我不会只依赖元数据,我想不要一个没有元数据就跑不了的工具。
  2. 字段血缘的DAG,不能独立于表级血缘。 理想情况下,只维护一份统一的血缘图。至于实现,可以有两种:1)把DAG做到字段粒度,通过一些转换, 可以计算出表级血缘的DAG。用关系型数据库的概念来做类比,就像先做一张明细表,在明细表的基础上聚合可以得到汇总表。2)通过属性图的形式来建模, 可以参照JanusGraph的文档。表和字段分别是两种类型的节点,同时另外还有两种类型的边, 其一是字段到表的所属关系,其二是字段与字段、表与表的血缘关系。

需要解答的问题

有了上面两点设计原则,我们就可以再来思考一下代码实现层面的细节了。有这么一些问题是要优先想明白的:

  1. 用什么样的数据结构来表示字段血缘? 之前表级别是用networkx中的DiGraph来存储DAG的,其中表是节点,而表级血缘是边。从表级别来说, 非常直观。那么字段呢?

一种可行的答案: 结合我以前做图数据库应用时的一些经验,汇总级别的操作,也就是针对图的OLAP,其实并不是那么好做。图数据库一般用Gremlin语言, 做OLAP都有些别扭,networkx的API我就更加不确定能否支持了。所以我个人更偏向属性图的建模形式。

  1. 如何处理SELECT *。比如下面这个SQL,我不知道tab2里有哪些字段。
INSERT OVERWRITE tab1
SELECT * FROM tab2;

一种可行的答案: 增加一个Column *的虚拟节点,作为特殊的字段。这样这里的字段血缘可以表示为tab2.* -> tab1.*

  1. 如何处理多表关联时,字段名没有写表/别名前缀的情况。比如下面这个SQL,我不知道col2是来自tab2还是tab3。
INSERT OVERWRITE tab1
SELECT col2
FROM tab2
JOIN tab3
ON tab2.col1 = tab3.col1

一种可行的答案: 维护两条血缘,tab2.col2 -> tab1.col2和tab3.col2 -> tab1.col2。同时,在这两条边上做特殊的标记, 表明需要元数据才能进一步剪裁。而前端可视化时,也可以将这种血缘用特殊的方式标记出来,比如绘制成虚线,悬浮时才显示。

功能实现的拆解

大方向上悬而未决的问题定下来了,接下来是具体的功能拆解:

  1. 字段逻辑的原子性操作识别:字段别名,CASE WHEN,函数,表达式等。
  2. 子查询的识别,以及子查询的血缘结果如何提升到语句级别的血缘。
  3. 多表关联时,字段到表的归属确定。
  4. 将语句级别的血缘信息,组装为SQL文件级别(文件级别可以包含多条SQL语句)。

拆解到这个程度之后,设计层面的事情就都搞定了。之后就是纯堆人力的问题,以及修复一些边边角角的bug,花时间总能搞定。

而今迈步从头越

所有这些,回过头看起来很显而易见的抉择,当时前后总计思考了大约3个月吧。最后花了一整个国庆假期,前后10天完成了编码。

字段血缘做下来,感悟很多:

  • 字段血缘是一个中等难度的项目,写出来还是很有成就感的。之前1.2版本,只包含表级血缘,大约400行Python代码。到1.3版本,暴增到约800行。 核心的抽象,增加了很多。之前比如Union语句,比如子查询,比如CTE,都不怎么需要特别关心,做字段血缘时就需要非常小心去处理。

  • 字段血缘需要更深入地了解SQL解析的过程。我用的sqlparse是不校验语法的,为了支持各种SQL方言,它只是尽力去解析,不保证结果一定合法。 所有有时候我需要微调一下parser,以便生成符合设想、更好操作的AST。比如窗口函数就是我自己定制的,原生的sqlparse会把窗口函数解析成两个独立的部分。 没有编译原理的基础知识,做这个会稍稍有些吃力。相比较做表级血缘的时候,其实AST对我来说就是个普通的数据结构,我会用就行了,不关心它是怎么生成的。

  • 字段血缘相比表级血缘,更需要可视化。表级别的,直接给出输入表和输出表,信息损失也不大。但如果字段血缘还是只有命令行界面的输出, 那就非常难以使用了。庆幸之前做了JavaScript的可视化方案改造,Cytoscape.js在换到字段血缘后几乎只是稍微改了一下配置, 就可以直接展示后端给出的新的数据结构了。这里是比我最初设想节省了时间的。

展望未来

SQLLineage还会有v1.4吗,我想肯定是会有的,毕竟还有源源不断的issue。之前没有社区的呼声和支持,字段血缘是不可能做出来的。 之后我也会持续地花时间去迭代,响应社区的需求。

现阶段主要考虑到的大的功能点包括:

  1. 为了更好做好字段血缘,元数据插件必不可少。目前的设想是,定义一个元数据类,一些接口,比如list_tables,list_columns。由用户个人来实现。 这样SQLLineage只要拿到用户提供的元数据类,就可以针对不准确的信息进行校准。系统可以维护一个最常用的Hive metastore的元数据类实现, 也作为其它实现的参考。

  2. 目前的很多Issue集中在对更多方言特殊语法的支持,Snowflake的Merge语句,比如SQLServer的Assignment Operator。要做更好的支持, 当前的sqlparse已经有点力不从心,我在它基础上做了非常多的monkey_patch,这个项目很可惜也已经不再活跃开发了。 暂时的一个想法是用Antlr来生成自己的parser,未来我会先用Java版本论证一下可行性。

  3. SQLLineage现在更多是作为Web应用,受到关注。这和我最开始主要考虑命令行应用+Python库的定位,其实有一定的背离。 在之前的版本中,我也有在刻意隐藏一些Python的API。因为字段血缘不做,API变动就会很大。现阶段,我认为SQLLineage作为Python库的潜力是没有完全释放的。 字段血缘相对稳定之后,Python API也是可以重点考虑的一个方向。

总之感谢社区的支持。开源是一件非常有意思、也有情怀的事情。Let's have some more.

Abstraction is good, but don't forget reality.

Copyright © reata.github.io 2023, powered by GastbyJS & Material UI