Spark SQL系列之SQL到Unresolved Logical Plan

  1. 摘要

  从Spark发展过程来看,Spark SQL模块无疑是Spark整个项目中最重要的模块之一,经过Spark项目的不断迭代发展,对Spark SQL模块已经做了很多优化,尤其是最近几次的发布来看,Spark 3.1.1 Release Note[1],Spark 3.2.0 Release Note[2]针对Spark SQL的改进、优化都是最活跃的模块之一,因此学习Spark SQL,了解其运行机制也是掌握Spark的一门必修课。

  本系列旨在带读者一起了解Spark SQL从一条SQL到最后作业执行的全路径分析,帮助读者从源码揭开Spark SQL的核心内幕。

  注明:本系列代码分析基于Spark V3.2.0[3]

  2. 引入

  先放一张Spark SQL非常经典的图,这张图说明了SQL在运行时的生命周期

  1.经过Parser阶段,被解析为一个逻辑计划(称为Unresolved Logical Plan)2.经过Analysis阶段,被转化为一个逻辑计划(称为Resolved Logical Plan)3.经过Optimzation阶段,被转化为优化后的逻辑计划(称为Optimized Logical Plan)4.经过Planning阶段,生成很多物理执行计划(称为Physical Plan)5.经过代价模型选取最优的物理执行计划6.经过Code Generation生成RDD7.RDD在Spark环境中执行

  

  首先从SQL Query/DataFrame到Unresolved Logical Plan时肯定需要对SQL进行解析,对SQL进行解析有多种不同的方式,在大数据领域比较流行的是利用Antlr4或者Apache Calcite做SQL解析,其中Spark SQL、Presto都使用了Antlr进行SQL解析,而Apache Flink、Apache Phoenix则借助Apache Calcite做SQL解析、优化等。为对接Antlr4,Spark自定义了g4文件[4],更多关于如何使用Antlr4不是本文的重点,感兴趣的同学可自行在网上搜索用法。

  3. 示例

  当在Spark SQL中使用如下SQL语句创建一张表时,Spark底层到底发生了什么?

  spark.sql("create table test(id int, name string) using parquet")

  跟随代码可以看到会调用SparkSession#sql方法,sql方法签名如下

  defsql(sqlText: String): DataFrame=withActive { val tracker=newQueryPlanningTracker val plan=trackerasurePhase(QueryPlanningTracker.PARSING) { // 进行SQL解析 sessionState.sqlParser.parsePlan(sqlText) } Dataset.ofRows(self, plan, tracker) }

  对于sessionState.sqlParser实际引用的是SparkSqlParser实例,其继承关系如下

  SparkSqlParser=> AbstractSqlParser=> ParserInterface

  为简单表达起见,=> 表示继承关系,ParserInterface定义了一些解析接口方法,包括根据SQL解析Plan,解析表达式,解析表名,解析函数等

  其中的调用路径如下

  SparkSession#sql=> AbstractSqlParser#parsePlan=> SparkSqlParser#parse=> AbstractSqlParser#parse

  其中最关键的一段代码是AbstractSqlParser#parsePlan

  overridedefparsePlan(sqlText: String): LogicalPlan=parse(sqlText) { parser=> astBuilder.visitSingleStatement(parser.singleStatement) match { caseplan: LogicalPlan=> plan case_=> val position=Origin(None,None) throwQueryParsingErrors.sqlStatementUnsupportedError(sqlText, position) } }

  对于这段代码可以看到其完成了sqlText到Spark中LogicalPlan的转化,LogicalPlan是一颗树形结构,记录了对应逻辑算子树节点的基本信息和基本操作,包括输

  入输出和各种处理逻辑等。继承关系如下所示

  TreeNode|-- QueryPlan // An abstraction of the Spark SQL query plan tree, which can be logical or physical.| |-- LogicalPlan // The base class for logical operators.| | |-- UnaryNode // A logical plan node with single child.| | |-- BinaryNode // A logical plan node with a left and right child.| | |-- LeafNode // A logical plan node with no children.| |-- SparkPlan // The base class for physical operators.| |-- UnaryExecNode| |-- BinaryExecNode| |-- LeafExecNode|-- Expression // An expression in Catalyst. |-- UnaryExpression // An expression with one input and one output. |-- BinaryExpression // An expression with two inputs and one output. |-- TernaryExpression // An expression with three inputs and one output. |-- LeafExpression // A leaf expression, i.e. one without any child expressions.

  顶层类TreeNode定义了一些遍历树(自顶向下、自底向上)、寻找某个子节点、替换某节点的方法,而其主要的子类包括QueryPlan和Expression,QueryPlan表示查询计划体系,Expression表示表达式体系,而QueryPlan的子类主要分为LogicalPlan逻辑执行计划和SparkPlan物理执行计划,LogicalPlan/SparkPlan又可进行进一步划分,这里不再赘述。

  继续回到之前的SQL到LogicalPlan的转化,仅仅靠Antlr的SQL解析肯定无法做到转化为Spark内部的LogicalPlan,Spark中肯定定义了某个类来完成这样的转化,这个类就是AstBuilder,其继承了SqlBaseBaseVisitor(该类由Antlr4自动生成,定义了访问SQL语句中不同节点的方法,子类可实现来自定义处理逻辑),同时获取该对象的方法astBuilder在AbstractSqlParser中声明为了抽象方法,子类SparkSqlParser进行了具体实现(SparkSqlAstBuilder)。

  接着调用路径分析,调用的AbstractSqlParser#parse方法中有一段模版代码(生成TokenStream、SqlBaseParser),在很多利用Antlr4做SQL解析的其他项目如elasticsearch-sql[5],sql-for-luence[6]均可找到,其不影响主链路分析,核心点在于

  astBuilder.visitSingleStatement(parser.singleStatement)这行代码,这行代码完成了sqlText到LogicalPlan的转化,下面摘取了AstBuilder类中的部分典型方法

  /** * Create an un-aliased table reference. This istypically usedfortop-level table references, * forexample: * {{{ * INSERT INTO db.tbl2 * TABLE db.tbl1 * }}} */ overridedefvisitTable(ctx: TableContext): LogicalPlan=withOrigin(ctx) { // 访问表,如将db.tbl1解析为UnresolvedRelation,UnresolvedRelation是LogicalPlan子类 UnresolvedRelation(visitMultipartIdentifier(ctx.multipartIdentifier)) } /** * 更新表的处理,如update语句 */ overridedefvisitUpdateTable(ctx: UpdateTableContext): LogicalPlan=withOrigin(ctx) { val table=createUnresolvedRelation(ctx.multipartIdentifier) val tableAlias=getTableAliasWithoutColumnAlias(ctx.tableAlias, "UPDATE") val aliasedTable=tableAlias.map(SubqueryAlias(_, table)).getOrElse(table) val assignments=withAssignments(ctx.setClause.assignmentList) val predicate=if(ctx.whereClause !=) { Some(expression(ctx.whereClause.booleanExpression)) } else{ None } // UpdateTable是LogicalPlan子类 UpdateTable(aliasedTable, assignments, predicate) } ...

  可以看到通过继承Antlr4自动生成的SqlBaseBaseVisitor类,并重写部分方法,便可以完成从SQL到Spark LogicalPlan的过程。

  最后看看一条SQL生成的Unresolved Logical Plan到底是什么样,可以直接使用生成一个SparkSqlParser,然后打印LogicalPlan

  val parser=newSparkSqlParserprintln(parser.parsePlan("create table test(id int, name string) using parquet"))println(parser.parsePlan("select * from test where id=1 and name='test'"))

  输出结果如下

  'CreateTableStatement [test], [StructField(id,IntegerType,true), StructField(name,StringType,true)], parquet, false, false'Project [*]+- 'Filter (('id=1) AND ('name=test)) +- 'UnresolvedRelation [test], , false

  可以看到对于对于第二条SQL而言,其根节点为Project,表示select,Project子节点为Filter,表示where,Filter子节点为UnresolvedRelation,表示test表名。其中'符号表示未解析的逻辑计划节点。如果想进一步Debug Unresolved LogicalPlan的生成,可参考SparkSqlParserSuite。

  4. 总结

  本篇文章分析了Spark如何将用户的SQL解析为Unresolved LogicalPlan的流程,其中的核心点就在于Spark定义了AstBuilder这个类来对接Antlr4生成SqlBaseBaseVisitor类,而后才能将SQL转化为Unresolved Logical Plan。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值