🚀 优质资源分享 🚀
学习路线指引(点击解锁) | 知识定位 | 人群定位 |
---|---|---|
🧡 Python实战微信订餐小程序 🧡 | 进阶级 | 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。 |
💛Python量化交易实战💛 | 入门级 | 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统 |
vivo 互联网服务器团队- Shuai Guangying
探究Presto SQL引擎 系列:第1篇《探究Presto SQL引擎(1)-巧用Antlr》介绍了Antlr的基本用法以及如何使用Antlr4实现解析SQL查询CSV数据,在第2篇《探究Presto SQL引擎(2)-浅析Join》结合了Join的原理,以及Join的原理,在Presto中的思路。
本文是系列第3篇,介绍基于 Antlr 实现where条件的解析原理,并对比了直接解析与代码生成实现两种实现思路的性能,经实验基于代码生成的实现相比直接解析有 3 倍的性能提升。
一、背景问题
业务开发过程中,使用SQL进行数据筛选(where关键词)和关联(join关键词)是编写SQL语句实现业务需求最常见、最基础的能力。
在海量数据和响应时间双重压力下,看似简单的数据筛选和关联在实现过程中面临非常多技术细节问题,在研究解决这些问题过程中也诞生了非常有趣的数据结构和优化思想。比如B树、LSM树、列式存储、动态代码生成等。
对于Presto SQL引擎,布尔表达式的判断是实现where和join处理逻辑中非常基础的能力。
本文旨在探究 where 关键词的实现思路,探究where语句内部实现的基本思路以及性能优化的基本思想。以where语句为例:where筛选支持and 、or 和 not 三种基础逻辑,在三种基础逻辑的基础上,支持基于括号自定义优先级、表达式内部支持字段、函数调用。看似简单,实则别有洞天。值得深入挖掘学习。
二、使用 Antlr 实现 where 条件过滤
对于Presto查询引擎,其整体架构如下:
其中,Parser&Analyzer就是Antlr的用武之地。任何的SQL语句,必须经过Parser&Analyzer这一步,所谓一夫当关万夫莫开。关于Antlr的背景及基础操作等内容,在《探究Antlr在Presto 引擎的应用》一文已有描述,不再赘述。
本文依然采用驱动Antlr的三板斧来实现SQL语句对where条件的支持。
对于where条件,首先拆解where条件最简单的结构:
and 和or作为组合条件筛选的基本结构。
6大比较运算符(大于,小于,等于,不等于,大于或等于,小于或等于)作为基本表达式。
接下来就是使用 Antlr 的标准流程。
2.1 定义语法规则
使用antlr定义语法规则如下 (该规则基于presto SQL语法裁剪,完整定义可参考presto SelectBase.g4文件):
querySpecification
: SELECT selectItem (',' selectItem)*
(FROM relation (',' relation)*)?
(WHERE where=booleanExpression)?
;
...
booleanExpression
: valueExpression predicate[$valueExpression.ctx]? #predicated
| NOT booleanExpression #logicalNot
| left=booleanExpression operator=AND right=booleanExpression #logicalBinary
| left=booleanExpression operator=OR right=booleanExpression #logicalBinary
;
predicate[ParserRuleContext value]
: comparisonOperator right=valueExpression #comparison
;
即where条件后面附带一个booleanExpression表达式规则,booleanExpression表达式规则支持基础的valueExpression预测、and和or以及not条件组合。本文的目的是探索核心思路,而非实现一个完成的SQL筛选能力,所以只处理and和or条件即可,以实现删繁就简,聚焦核心问题的目的。
2.2 生成语法解析代码
参照 Antlr 的官方文档,使用预处理好的 Antlr命令处理g4文件,生成代码即可。
antlr4 -package org.example.antlr -no-listener -visitor .\SqlBase.g4
2.3 开发业务代码处理 AST
2.3.1 定义语法树节点
在了解了表达式构成后,先定义两个基础的SQL语法树节点,类图如下:
这两个类从结构上是同构的:左右各一个分支表达式,中间一个运算符。
2.3.2 构建语法树
在AstBuilder实现中,新增对logicalBinary, comparison相关语法的解析实现。这些工作都是依样画葫芦,没有什么难度。
@Override
public Node visitComparison(Select1Parser.ComparisonContext context)
{
return new ComparisonExpression(
getLocation(context.comparisonOperator()),
getComparisonOperator(((TerminalNode) context.comparisonOperator().getChild(0)).getSymbol()),
(Expression) visit(context.value),
(Expression) visit(context.right));
}
@Override
public Node visitLogicalBinary(Select1Parser.LogicalBinaryContext context)
{
return new LogicalBinaryExpression(
getLocation(context.operator),
getLogicalBinaryOperator(context.operator),
(Expression) visit(context.left),
(Expression) visit(context.right));
}
通过上面的两步,一个SQL表达式就能转化成一个SQL语法树了。
2.3.3 遍历语法树
有了SQL语法树后,问题就自然而然浮现出来了:
a) 这个SQL语法树结构有什么用?
b) 这个SQL语法树结构该怎么用?
其实对于SQL语法树的应用场景,排除SQL引擎内部的逻辑,在我们日常开发中也是很常见的。比如:SQL语句的格式化,SQL的拼写检查。
对于SQL语法树该怎么用的问题,可以通过一个简单的例子来说说明:SQL语句格式化。
在《探究Antlr在Presto 引擎的应用》一文中,为了简化问题采取了直接拆解antlr生成的AST获取SQL语句中的表名称和字段名称,处理方式非常简单粗暴。实际上presto中有一种更为优雅的处理思路:AstVisitor。也就是设计模式中的访问者模式。
访问者模式定义如下:
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。
这个定义落实到SQL语法树结构实现要点如下:即SQL语法树节点定义一个accept方法作为节点操作的入口(参考Node.accept()方法)。定义个AstVisitor类用于规范访问节点树的操作,具体的实现类继承AstVisitor即可。基础结构定义好过后,后面就是万变不离其宗了。
两个类核心框架代码如下:
public abstract class Node
{
/**
* Accessible for {@link AstVisitor}, use {@link AstVisitor#process(Node, Object)} instead.
*/
protected R accept(AstVisitor visitor, C context)
{
return visitor.visitNode(this, context);
}
}
public abstract class AstVisitor
{
pro