
Presto是一个高性能的分布式SQL查询引擎,应用于PB级数据的实时计算分析场景。Presto支持ANSI SQL, 用户可以直接使用SQL进行数据查询和计算。生成查询执行计算的主要流程如下图:

从提交查询到执行计划生成包含了词法分析、语法分析、语义分析、逻辑执行计划生成、执行计划优化、分布式执行计划分生成等部分,Sql Parser主要提供了词法分析和语法分析功能,下面我们一起来探究一下Presto的SQL Parser实现。
ANTLR简介
ANTLR(全称:ANother Tool for Language Recognition)是目前非常流行的语言识别工具,使用Java语言编写,基于LL(*)解析方式,使用自上而下的递归下降分析方法。通过输入语法描述文件来自动构造自定义语言的词法分析器、语法分析器和树状分析器等各个模块。ANTLR使用上下无关文法描述语言,文法定义使用类似EBNF的方式。
ANTLR除能够自动构建语法分析树外,还能生成基于Listener(监听者模式,通过节点监听,触发处理方法)和Visitor(访问者模式,主动遍历)的树遍历器。访问者模式遍历语法树是一种更加灵活的方法,可以避免在文法文件中嵌入繁琐的动作,使解析与应用逻辑代码分离,这样不但文法的定义更加简洁清晰,而且可以在不重新编译生成语法分析器的情况下复用相同的语法,甚至能够采用不同的程序语言来实现这些动作。
语言识别工具还有很多种,比如Lex和Yacc, 还有Apache Calcite里面使用的JavaCC等等,在这里就不进行一一比较了。ANTLR的应用非常广泛,比如Hive、Presto和SparkSQL等的SQL Parser模块都是基于ANTLR构建的。
Visitor模式
访问者模式(Visitor Pattern)是一种将操作与对象结构分离的软件设计模式,提供作用于某种对象结构上各元素的操作,可以使我们在不改变元素结构的前提下,定义作用于元素的新操作。
这种模式的工作方法如下:假设有一个由许多元素Node构成的对象结构Tree,这些Node类都拥有一个accept方法用来接受访问者对象Visitor的访问;Visitor类是一个接口,它拥有一个visit方法,这个方法对访问到的Tree中不同类型的Node作出不同的反应;在对Tree的一次访问过程中会遍历整个Tree,对遍历到的每个Node都调用accept方法,在每个元素的accept方法中回调Visitor的visit方法,从而使Vistior得以处理Tree的每个Node;可以针对Tree设计不同的Visitor实现类来完成不同的操作。
在后面的学习中我们将会看到,在Presto引擎中许多地方都会用到Visitor模式。
关于ANTLR的使用入门实例大家可以去官网看一下“基于ANTLR4实现的计算器“案例,这里就不进行详细展开了,下面我们从源码层面来分析Presto的Sql Parser模块是如何实现的。
Presto Sql Parser源码分析

Presto使用ANTLR4编写的SQL语法文法的定义在presto-parser
模块的SqlBase.g4
文件中,将Parser模块编译之后会如下图的一些类,其中比较重要的类有词法分析器(SqlBaseLexer)、语法分析器(SqlBaseParser)、和访问者类(SqlBaseVisitor接口与SqlBaseBaseVisitor类)。

词法分析
Presto的Sql词法分析发生在SqlParser
类的invokeParser
方法中。
private Node invokeParser(String name, String sql, Function<SqlBaseParser, ParserRuleContext> parseFunction)
{
try {
//使用词法分析器SqlBaseLexer进行词法分析,产生token序列
SqlBaseLexer lexer = new SqlBaseLexer(new CaseInsensitiveStream(new ANTLRInputStream(sql)));
CommonTokenStream tokenStream = new CommonTokenStream(lexer);
SqlBaseParser parser = new SqlBaseParser(tokenStream);
//在语法分析器上,添加一个异常处理的监听器PostProcessor
parser.addParseListener(new PostProcessor());
//词法分析器和语法分析器都添加出错时,抛运行时ParsingException异常的监听器ERROR_LISTENER
lexer.removeErrorListeners();
lexer.addErrorListener(ERROR_LISTENER);
parser.removeErrorListeners();
parser.addErrorListener(ERROR_LISTENER);
//生成抽象语法树
ParserRuleContext tree;
try {
// first, try parsing with potentially faster SLL mode
parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
tree = parseFunction.apply(parser);
}
catch (ParseCancellationException ex) {
// if we fail, parse with LL mode
tokenStream.reset(); // rewind input stream
parser.reset();
parser.getInterpreter().setPredictionMode(PredictionMode.LL);
tree = parseFunction.apply(parser);
}
//下面开始进行语法分析
return new AstBuilder().visit(tree);
}
catch (StackOverflowError e) {
throw new ParsingException(name + " is too large (stack overflow while parsing)");
}
}
语法分析
Presto使用Visitor模式对SQL语句进行语法分析,上面代码中AstBuilder
类继承自访问者类SqlBaseBaseVisitor
,调用visit方法开始对整个tree进行遍历分析,我们以下面这个Select语句进行深入了解。
SELECT custkey FROM orders WHERE totalprice > 100.0
这个SQL语句语法解析之后生成的结构可以通过IDEA的ANTLR Preview插件生成可视化的解析树结构如下图:

在Presto中,SQL语句经过解析,生成的抽象语法树节点都以Context结尾来命名,这个Sql语句生成的语法解析树如下图:

其中SingleStatementContext是根节点,AstBuilder访问到SingleStatementContext时调用visitSingleStatement并没有做什么,只是递归访问子节点。遍历到QuerySpecificationContext节点时开始出现多个子节点,AstBuilder从左到右递归遍历访问子节点。左边这个SelectSingleContext子树对应Select表达式中选择的列custkey,中间的RelationDefaultContext子树对应数据表orders,右边的PredicateContext子树对应where条件中的表达式。
从这个实例中我们可以发现语法分析实际上是一个递归调用的过程,构造一个个的节点,最终形成一个抽象语法树,实例中的这个SQL语句经过语法分析之后会返回一个Statement
的子类Query
对象。
当我们需要开发新的语法支持时,首先需要在SqlBase.g4
中添加文法规则,重新编译生成词法分析器、语法分析器和访问者类,然后在AstBuilder
等类中添加相应的访问逻辑,最后添加执行逻辑。
结语
本文简单介绍了ANTLR这一个语言识别工具,以及分析Presto Sql Parser模块的实现源码,最后,结合具体案例的语法树可视化展示,以加深大家对语法分析过程的理解。在接下来的学习中,还将计划给大家带来Analyzer、Planner、Optimizer等模块设计与实现的探究。
参考资料
ANTLR
Extended Backus-Naur form - Wikipedia
Presto | Distributed SQL Query Engine for Big Data
Visitor pattern - Wikipedia