今天是2023年2月12日,据我上一次发表文章已经过去了半年多的时间,这篇文章主要是针对过去一年里参与解析工作的一些感悟,希望大家可以从中有所收获。
目前博主在公司内具体研究方向为SQL解析与应用,在 BIG 产品中目前的SQL列级血缘解析在客户现场经验证已达到99%的准确率(未支持的特殊语法属于变量)。
由于讨论范围属于公司核心技术领域,本文只是稍微写一些我做解析一年多来的认识,可以给没有做过SQL血缘解析、或准备做其他解析的同学一些思路。我在2022年前3个月份发布了一些文章,那些都是我刚接触这一块时所记录的文章,现在看里面有许多错误的地方,那些文章就可以不用再看了。
SQL血缘解析所带来的价值和应用,这里就不再阐述了,这里直接上重点部分。
在此之前你可能需要先了解一些概念:
- DSL
- 词法解析,语法解析
- 抽象语法树(AST)
- 关系代数和逻辑执行计划
如果你不了解这几个概念,或者不是太熟悉,建议先在网上查阅一些资料。
那么,针对SQL的血缘解析,这里要分两个东西去看:解析 和 血缘 。当然,是 解析在前、血缘在后。
很明显,解析是血缘的基础,血缘是对解析的应用,如果把“SQL血缘解析”解释的再详细一些,那么就是“解析SQL并提取其中的血缘关系”。那么,当我们拿到血缘解析这个命题的时候,要分两步去做:
- 解析SQL
- 提取血缘
那么这里就牵出两个新的问题
- 如何解析SQL
- 如何提取血缘
当我们最早学习Mysql的时候,或多或少都有敲过它的一些语法,拿最简单的:SELECT c1 FROM t1;
,如果我们把FROM
不小心打成了FRPM
,这个时候把命令提交给数据库引擎后就会报错,提示错误的语法等提示,那么这里需要思考的是,引擎它是怎么知道我不应该输入 FRPM
,而是 FROM
。
无论是SQL,还是JAVA还是其他语言,本质上都是一种DSL(domain special language),而我们在输入某种特定的DSL时,其相应的程序第一步要做的事情就是校验你输入的语句是否符合正确的语法,简而言之就是会去解析你输入的语句。
在《编译原理》中,通常对文本的解析会包含词法分析,语法分析和语义分析,在语法分析和语义分析阶段都会产生相应的抽象语法树AST。因为对于一个文本 string
,我们对其里面的信息是无从下手的,所以我们需要将其转化成一种对象结构,就是指这里的抽象语法树AST。
(如果你对词法分析、语法分析、抽象语法树还不了解的话,需要先了解这些知识)
由此可知,我们解析一条sql,有以下两个作用:
- 判断该sql的语法是否正确
- 得到该sql的抽象语法树AST
那么我们可以通过哪些工具解析sql得到ast呢?
- Antlr
- Calcite
- Druid
- (其他类似druid手写解析器)
所以,这里建议大家可以先了解下 Antlr 和 calcite,这里有我的一个对比
我主要详细说下 Antlr 的,去年1月份写的那篇文章可能已经过期了, Antlr 的词法文件还是很全的,可以在官方社区里找到:
antlr/grammars-v4/tree/master/sql
我们可以看到主流的sql语法这里都支持了,对于其他的,例如sparkSQL,clickhouse内部也是使用的antlr,在他们对应的代码中我们也可以找到相应的g4文件。
Antlr的使用这里我也不过多赘述,起码我们解析一条sql,拿到生成的抽象语法树应该不成问题。
6 条评论
怎么提取 列血缘关系呀, 我用antlr 能解析出 select语句的信息了,包括嵌套子查询等. 怎么样的逻辑提取列血缘并用什么结构存储呢?
antlr解析出来的是SQL1:1抽象语法树,血缘解析属于是语义分析了(作用域分析),需要再设计一套AST,这套AST具备血缘分析能力;这句话的理解你可以类比Calcite里的SqlNode -> 校验 -> RelNode(SqlNode类似于Antlr生成的那一层AST)
能否请教下,有什么高效的方法解析sql statement的类型(可以支持各种方言sql的)?
文章里还没写,我最近会更新。解析sql的话就使用一些解析器,antlr或calcite,antlr有相应的词法文件g4,也比较容易扩展;calcite内部用parse.jj,扩展能力比antlr差点,但是速度比antlr快;如果你考虑易扩展且高效解析,可以混合使用两种解析器
我目前是在是用antlr的监听器模式来解析sql statement,但是在解析pPostgreSQL的insert into语句时总会是返回SELECT类型,以下是核心代码,望不吝赐教。
@Override
public void enterEveryRule(ParserRuleContext ctx) {
if(ctx instanceof PostgreSQLParser.VariableshowstmtContext) {
this.sqlStatementTypeEnum = SqlStatementTypeEnum.SHOW;
} else if(ctx instanceof PostgreSQLParser.ExplainstmtContext) {
this.sqlStatementTypeEnum = SqlStatementTypeEnum.EXPLAIN;
} else if(ctx instanceof PostgreSQLParser.CreatestmtContext || ctx instanceof PostgreSQLParser.DropstmtContext || ctx instanceof PostgreSQLParser.AlterseqstmtContext) {
this.sqlStatementTypeEnum = SqlStatementTypeEnum.DDL;
}else if(ctx instanceof PostgreSQLParser.DeletestmtContext || ctx instanceof PostgreSQLParser.UpdatestmtContext || ctx instanceof PostgreSQLParser.InsertstmtContext) {
this.sqlStatementTypeEnum = SqlStatementTypeEnum.DML;
}else if (ctx instanceof PostgreSQLParser.SelectstmtContext){
this.sqlStatementTypeEnum = SqlStatementTypeEnum.SELECT;
}
}
无论是访问者模式还是监听者模式,定义的每一个语法都是一个可以进入的节点,比如你使用的是监听者模式,想要监听的语法节点是PostgreSQLParser.SelectstmtContext,这里可以:
@Override
public void enterSelectstmtContext(PostgreSQLParser.SelectstmtContext ctx) {
// do something when entering this node
}
或者
@Override
public void exitSelectstmtContext(PostgreSQLParser.SelectstmtContext ctx) {
// do something when exiting this node
}
你可以拿ctx里的一些东西,具体有什么东西需要结合语法里的定义,都是ParserRuleContext的子类,你也可以结合IDEA的antlr插件更清晰的看到抽象语法树是什么样的