0、前言
之前写了篇 基于jsPlumb.js的模仿sqlFlow数据血缘图的前端页面,是利用jsplumb.js模仿sqlflow的前端。
然后有些小伙伴想看后端代码,当时第一版代码写得太乱了,所以就没直接发。磨了1个月,虽然还有bug但是也算基本能跑了。
github代码地址:源代码丢失,待重新编写后开源
1、项目整体概述、思路
- 需求:完成类似sqlflow这样的,根据sql
离线
来生成粒度最小为字段级的数据血缘关系,并可视化。输入的sql暂时为HiveSQL。 - 思路:离线解析,所以hive自带的hook LineageInfo和LineageLogger这两个工具类就用不上了(利用hive自带的钩子去解析血缘也是网上最多的方案)。一些开源的解析器(druid等)不太适合hive,所以只能自己写,一条sql在hive中大概会经历以下变化:
ANTLR
语义分析
原始SQL
AST
逻辑计划
物理执行计划
- 第一步:Hive用ANTLR语法定义的词法和文法文件来进行解析,生成抽象语法树。(语法分析也是在这一步做的)
- 后面几步:得到AST后,发现后面几步在hive源码中依赖太重了,我想从源码中剥离出来很困难,所以只能基于AST来写数据血缘了。
- 项目结构介绍「参考下图」
- parser包:自己写的解析AST、生成数据血缘的核心包
- ql包:从hive 3.5.2中剥离出来的包用于将sql解析AST
- bean、service、controller、utils:不需要解释吧?
- resources:前端。来自我的基于jsPlumb.js的模仿sqlFlow数据血缘图的前端页面文章,有改动。static的index包下的index.html就是前端了
- 如何部署:下载项目后,启动
HiveSqlDataLineageApplication
类。然后浏览器访问http://localhost:12284/index/index.html
,在页面的textarea中输入待解析的一条查询SQL,点击旁边的按钮visualize即可。 - 适用范围及注意事项:
目前仅支持select……from…… 这样的查询语句。
- 0.一切的基础是基于Hive解析模块生成的AST,若解析失败后面的血缘信息也就自然没有。 所以Hive不支持的函数(spark的left函数等)、中文需要自行处理。
- 对于CTE查询语句请删除 with部分
- 尽量给查询的表、查询的列都起别名
- select * 尽量不要写,因为得连接元数据库。
- 若查找的列名为中文,hive解析模块是解析不动的!本来想做将中文列名转换为拼音的的功能,但只写了工具类没做实现
2、项目细节
在数据血缘中,我们只在乎字段的流向,所以关于Where,Group by,Order by等等不影响结果的部分都可以无视掉,可以在AST中剪枝方便我们找出AST的规律。只把关注点集中在Select 、From、 Join(left、inner等)、 Union(union all)这几个关键词上。
数据血缘我们很容易想到是有向无环图(DAG),并且查询语句sql最终查询结果一般都是只有一个结果表。
所以对于无中间表的数据血缘我们只需要找出所有的起源表和一张结果表及起源表与结果表字段间的映射关系即可。
2.1、找出结果表
由于是无中间临时表的情况,观察AST发现。在查询语句中,AST一般都是这样,TOK_FROM和TOK_INSERT是兄弟节点。TOK_FROM就是from,是原表。TOK_INSERT就是 select选取的结果集。
TOK_QUERY
TOK_FROM
TOK_INSERT
并且无论sql咋复杂嵌套,最外层的TOK_INSERT一般都是最终生成的结果集(除了union的情况后面会说)。所以我们可以先解析TOK_INSERT下TOK_INSERT。
TOK_INSERT分支上一般只有两个子分支。TOK_DESTINATION不用关注且无用,TOK_SELECT就是结果表的所有列名所在地,每一个列名由下每个TOK_SELEXPR。TOK_SELEXPR就是列名的所在地,也是我们解析的大头。
TOK_INSERT
TOK_DESTINATION
TOK_SELECT
TOK_SELEXPR一般有以下情况
- 列名(带表名或者起别名排列组合四种情况)
- 被函数包裹(case、if也算)
- 常量和一些可以视为常量的由函数生成的列(如current_timestamp函数)
列名的四种情况分情况讨论解析即可。
函数主要是解析出是什么函数,方便在该列没有起别名的情况下用 函数名(列名)
做别名。一些特殊函数可能需要为其单独写分支处理。
常量单独解析即可因为AST结构和上面两类不一样。
具体参考代码的SelexprParser类。
结果表的特殊情况就是如果是 查询语句 unionall 查询语句,导致 AST结果改变失去顶级查询TOK_INSERT下的TOK_SELECT的TOK_SELEXPR。变成TOK_SETCOLREF,反正最后就打捞不到结果集。
TOK_QUERY=954
TOK_FROM=840
TOK_SUBQUERY=1026
TOK_UNIONALL=1082
TOK_QUERY=954
TOK_UNIONALL=954
TOK_INSERT=859
TOK_DESTINATION=810
TOK_SELECT=983
TOK_SELEXPR=985
TOK_SETCOLREF=990
我们得从TOK_FROM中打捞结果表了。这种情况下,由于union关键字(TOK_UNIONALL)可以嵌套。我们要从TOK_FROM递归去找最深处的TOK_UNIONALL下的TOK_QUERY节点。具体参考代码 CoreParser类的genRSTable()方法
2.2、找出起源表
起源表就有很多了。
分析ast可以发现,TOK_FROM是一系列子查询、join操作、union等操作的集合地。TOK_FROM通过嵌套大量的子查询、join、union来实现sql查询操作。在TOK_FROM中,还是以TOK_QUERY作为基本单元,以TOK_UNIONALL、TOK_SUBQUERY、TOK_LEFTOUTERJOIN等作为粘合剂,最终形式一个大的临时表即最终查询表供最终查询使用。
TOK_QUERY
TOK_FROM
TOK_JOIN|TOK_SUBQUERY|TOK_TABREF
TOK_INSERT
观察AST后,我计划从最大的TOK_FROM下手,提取最外层TOK_FROM里的TOK_QUERY单元作为起源表。除此之外,最外层TOK_INSERT里的结果集若带表名,虽然往往带的是原表名的别名,但是可以在后面将别名映射到原表名。
关于表名的解析,带不带库名也分为四种情况,如下图按情况分类讨论即可。
下面是没整理过的关键字解析,有些乱想看就看看吧!
- union从血缘的角度很简单,因为union本身限制,特别在hive中列名和列数都必须一样(hive官方文档说的),所以就更简单了。在无中间表的情况下我们直接忽视。
- 子查询。子查询指示其第零个子节点是TOK_QUERY或TOK_UNIONALL。TOK_SUBQUERY出现在TOK_UNIONALL、TOK_JOIN、TOK_FRO的第零个子节点。在无中间表的情况下我们碰到子查询只需要按TOK_QUERY查询的方法即可(其实就是直接不管)。
- join:
对于血缘来说join是怎样的并无所谓,leftjoin、right join 、inner Join(TOK_JOIN)并无不同。join不会显示临时表,因为join的目的是基于过滤条件筛选后,将两张表的列名成一张新表。血缘不在乎过滤,所以可以视为从join的两张表中打捞源表。
join的表类型有两种情况 - 直接join一个基本表:
left join db.tb c
,这种情况应该直接添加生成源表,这种情况join的表都是在最终查询用的。我们 - 要不在构建RS表的时候顺便构建origin表,
- 要不再rs表构建完后再利用rsRelation带表名的特征构建,这个感觉麻烦
- join的是一个子查询,这种情况毕竟多,而且子查询又可能嵌套。
两种类型的表会有三种组合
- join两个表均是基本表,两边都是基本表的情况,似乎只需要将表名打捞出.来当起源表即可。基本表join一般是最终查询中用到,我们先把表名打捞出来并记录映射关系,先生成没有字段集合的表集合,之后从rs中找出字段形成新表
TOK_LEFTOUTERJOIN=881
TOK_TABREF=1062
TOK_TABNAME=1061
aa_aar_safwew =24
zfvw=24
TOK_TABREF=1062
TOK_TABNAME=1061
aa_aa_info =24
abcc=24
and=36
……(on的条件)
- join两个表一个基本表一个子查询,这会导致parseVanillaTOK_QUERY无法解析
TOK_QUERY
TOK_FROM
TOK_JOIN
TOK_TABREF
TOK_SUBQUERY
TOK_INSERT
- join两个表均是子查询,子查询最终还是可以vanillaTOK_QUERY解析出结果,不会被parseVanillaTOK_QUERY遗漏
TOK_JOIN
TOK_SUBQUERY
TOK_SUBQUERY
TOK_QUERY
TOK_FROM
TOK_TABREF
TOK_INSERT
- 还有join生成的表再次被join,但伪血缘仅仅是需要打捞不需要考虑
总的来说遇到TOK_JOIN、TOK_LEFTOUTERJOIN,还是按照之前的想法,找到最底层的TOK_LEFTOUTERJOIN、TOK_JOIN,然后打捞源表
TOK_JOIN第零个子节点可以为TOK_TABREF、TOK_SUBQUERY,还可以嵌套其他TOK_JOIN
TOK_FROM|TOK_LEFTOUTERJOIN
TOK_JOIN
TOK_TABREF=1062
TOK_TABNAME=1061
a_bcdd=24
aa_b_cccc_va_irgwev_amanasdfd=24 a=24
TOK_SUBQUERY|TOK_TABREF|TOK_LEFTOUTERJOIN
2.3、关系
n个起源表和一个结果表搞定后,就需要构建关系了。这边基于字段分了四步:
- 先找 originTable的某个set里的字段名称和RST中的字段名相同的字段去构建大部分关系
- 将原始的rsNameToAliasRelations 的表名替换成起源表名,构建关系
- 处理被遗弃的 rsAbandonedRSColsMap
- originColToAliasRelations 检查是否还有遗漏
具体流程参考下图: