blood
血缘解析是数据治理中很关键的一环,本文着重讲解血缘解析的思路,如何把一段sql进行字段级别的解析,最终插入到数据库的数据表中,如下所示
target_tab | target_col | source_tab | source_col | source_flag | is_valid | calc_meth |
---|---|---|---|---|---|---|
一、AST抽象语法树
一段sql是如何被解析执行的呢?
无论是关系型数据库还是spark、hive等大数据引擎,第一步都是生成AST抽象语法树,流程如下
- 词法分析:分析量化那些本来毫无意义的字符流,将它们翻译成离散的字符组(也就是一个个的token),包括关键字、标识符、符号和操作符供语法分析器使用,如sql语句中的select、as、from等等都是token
- 语法分析:在分析字符流的时候,Lexer不关心所生成的单个token的语法意义及其上下文之间的关系,其一方面验证语法规则是否正确,如果正确,另一方面使用预定义的语法规则(如Mysql、Oracle语法规则)来构建AST抽象语法树
- AST构建:根据语法规则和词法单元序列,递归地构建AST,每个语法规则对应一个AST节点,而语法规则的非终结符对应AST的父节点,终结符对应AST节点的叶子节点
一般的AST抽象语法树如下所示
SELECT
|
[Columns]
/ | \
[Column] [Column] [Column]
| | |
id name age
|
FROM
|
[Table]
|
users
try{
statements = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL);
}catch (Exception e){
System.out.println("can't parser by druid mysql"+e);
}
上述代码是用druid sql来解析,常用的解析工具还有antlr4
二、血缘关系类
- 新增对象进行血缘关系的存储,提供一些getter/setter方法
name | desc |
---|---|
targetColumnName | 目标字段 |
sourceDbName | 字段来源Db |
sourceTableName | 字段来源表 |
sourceColumnName | 字段来源列 |
expression | 表达式,字段的加工过程 |
isEnd | 是否结束标识 |
三、递归迭代
- 通过递归迭代获取语法树结构,插入到血缘关系对象中,最终的叶子节点即为最终的血缘信息
比如说一段Sql传进来后,会先解析为AST抽象语法树
1、先传入Sql到解析入口,判断Sql是单独select语句还是包含union的select语句,若是包含union,通过 SQLUnionQuery.getLeft()和getRight()方法拆分union语句,再把拆分的语句放到解析的入口
columnLineageAnalyzer(((SQLUnionQuery) sqlSelectQuery).getLeft().toString(),node);
columnLineageAnalyzer(((SQLUnionQuery) sqlSelectQuery).getRight().toString(),node);
2、把单独的select语句或者union拆分的select语句传进来后,首先获取字段列表
List<SQLSelectItem> selectItems = sqlSelectQueryBlock.getSelectList();
然后遍历字段列表,通过getAlias获取字段的别名,若没有别名即为本身,别名就是targetColumnName,可以调用血缘关系对象的setTargetColumnName()方法
3、通过getExpr()获取字段的名称,也就是as之前的内容,若有加工过程也可获取到,然后调用setExpression()
4、解析表达式,上一步getExpr可能得到的字段是有多个字段加工而来,需要进行处理,解析表达式有如下种类,针对不同的类型有不同的处理方法,最终调用setSourceColumnName
- 方法
- 聚合
- case
- 比较
- 表达式
- 列
- 赋值表达式
- 数字
- 字符
- 标量子查询
private static void handlerExpr(SQLExpr sqlExpr,TreeNode<LineageColumn> itemNode) {
//方法
if (sqlExpr instanceof SQLMethodInvokeExpr){
visitSQLMethodInvoke( (SQLMethodInvokeExpr) sqlExpr,itemNode);
}
//聚合
else if (sqlExpr instanceof SQLAggregateExpr){
visitSQLAggregateExpr((SQLAggregateExpr) sqlExpr,itemNode);
}
//case
else if (sqlExpr instanceof SQLCaseExpr){
visitSQLCaseExpr((SQLCaseExpr) sqlExpr,itemNode);
}
//比较
else if (sqlExpr instanceof SQLBinaryOpExpr){
visitSQLBinaryOpExpr((SQLBinaryOpExpr) sqlExpr,itemNode);
}
//表达式
else if (sqlExpr instanceof SQLPropertyExpr){
visitSQLPropertyExpr((SQLPropertyExpr) sqlExpr,itemNode);
}
//列
else if (sqlExpr instanceof SQLIdentifierExpr){
visitSQLIdentifierExpr((SQLIdentifierExpr) sqlExpr,itemNode);
}
//赋值表达式
else if (sqlExpr instanceof SQLIntegerExpr){
visitSQLIntegerExpr((SQLIntegerExpr) sqlExpr,itemNode);
}
//数字
else if (sqlExpr instanceof SQLNumberExpr){
visitSQLNumberExpr((SQLNumberExpr) sqlExpr,itemNode);
}
//字符
else if (sqlExpr instanceof SQLCharExpr){
visitSQLCharExpr((SQLCharExpr) sqlExpr,itemNode);
//标量子查询
} else if (sqlExpr instanceof SQLQueryExpr) {
columnLineageAnalyzer(sqlExpr.toString(), itemNode);
}
}
5、字段处理完后,处理table,table有如下几种类型,每一种的处理方式也不一样
- 单表:获取db、tableName,调用血缘关系的set方法
- 子查询:子查询中间是一段Sql,再次传入血缘解析的入口
- 表连接:通过getLeft()和getRight()获取关联的表,关联的表又有单表、子查询、表连接、union这几种类型,迭代即可
- union:传入血缘解析入口即可
if (isContinue.get()){
// 获取表
SQLTableSource table = sqlSelectQueryBlock.getFrom();
// 普通单表
if (table instanceof SQLExprTableSource) {
// 处理最终表---------------------
handlerSQLExprTableSource(node, (SQLExprTableSource) table);
} else if (table instanceof SQLJoinTableSource) {
//处理join
handlerSQLJoinTableSource(node, (SQLJoinTableSource) table);
} else if (table instanceof SQLSubqueryTableSource) {
// 处理 subquery ---------------------
handlerSQLSubqueryTableSource(node, table);
}else if (table instanceof SQLUnionQueryTableSource) {
// 处理 union ---------------------
handlerSQLUnionQueryTableSource(node, (SQLUnionQueryTableSource) table);
}
}