Spark-SQL解析

本文深入解析Spark SQL如何利用Antlr4进行SQL语句的解析,从词法分析到语法树构建,再到未解析逻辑计划的生成,详细阐述了Spark-SQL解析流程及Antlr4的应用。

Spark-SQL解析

如下图所示,Spark-SQL解析总体分为以下几个步骤:解析(Parser)、绑定(Analysis)、优化(Optimization)、执行(Physical)、生成RDD(RDDs)。接下来,我们先介绍解析部分,对于绑定、逻辑计划的优化、物理执行计划、生成RDD后面再专门介绍。
在这里插入图片描述

Antlr4

一、简介

最新的Spark-Sql解析模块为spark-catalyst_2.11,通过Antlr4(Another Tool for Language Recognition)框架来实现。ANTLR 是用JAVA写的语言识别工具,它用来声明语言的语法。它的语法识别分为两个阶段:

1.词法分析阶段 (lexical analysis)

对应的分析程序叫做 lexer ,负责将符号(token)分组成符号类(token class or token type)

2.解析阶段

根据词法,构建出一棵分析树(parse tree)或叫语法树(syntax tree)
图1

二、语法

以下是四则运算的例子,Math.g4

grammar Math;  //声明语法头,类似于java类的定义

@header{package com.zetyun.aiops.core.math;} //在运行脚本后,生成的类中自动带上这个包路径,避免了手动加入的麻烦。

prog : stat+;

stat: expr NEWLINE          # printExpr    //定义规则:这是核心,表示规则,以 “:” 开始, “;” 结束, 多规则以 "|" 分隔。
    | ID '=' expr NEWLINE   # assign
    | NEWLINE               # blank
    ;

expr:  expr op=('*'|'/') expr   # MulDiv 
| expr op=('+'|'-') expr        # AddSub
| INT                           # int
| ID                            # id
| '(' expr ')'                  # parens
;

MUL : '*' ; // assigns token name to '*' used above in grammar
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
ID : [a-zA-Z]+ ;
INT : [0-9]+ ;
NEWLINE:'\r'? '\n' ;
WS : [ \t]+ -> skip;
字符含义
() : 产生式组合
?  : 产生式出现0或1次
*  : 0或多次
+  : 1或多次
.   : 任意一个字符
~  : 不出现后面的字符
..  : 字符范围

三、编译

利用antlr4-maven-plugin插件即可完成自动编译。

        <dependency>
            <groupId>org.antlr</groupId>
            <artifactId>antlr4-runtime</artifactId>
            <version>4.7</version>
        </dependency>
  
            <plugin>
                <groupId>org.antlr</groupId>
                <artifactId>antlr4-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>antlr4</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <visitor>true</visitor>
                    <sourceDirectory>../stream-catalyst/src/main/antlr4</sourceDirectory>
                </configuration>
            </plugin>

可通过IDEA的maven插件编译,之后可在根目录\target\generated-sources\antlr4\com\hikvs\bigdata\catalyst\parser下产生以下JAVA文件:
在这里插入图片描述
其中MathLexer是词法分析器、MathParser是语法分析器、MathVisitor和MathBaseVisitor分别是访问者接口和访问者类。

四、遍历模式

Antlr4的遍历模式分两种:

1.Listener(观察者模式)

优点:通过节点监听,触发处理方法,用户不需要显示控制语法树的顺序,实现简单。
缺点:不能显示控制遍历语法树的顺序;没有返回值,需要使用map、栈等结构在节点间传值

2.Visitor(访问者模式)

优点:主动遍历,用户可以显示定义遍历语法树的顺序、有返回值。

比较两种模式,一般采用访问者模式,可以显示控制遍历树的顺序。访问者类的作用是用于遍历整个语法树,然后进行相关操作,用户可以自己实现访问者类来定义自己需要的功能。

public class MathVisitorTest extends MathBaseVisitor<Integer> {
    Map<String, Integer> memory = new HashMap<String, Integer>();
    @Override
    public Integer visitPrintExpr(MathParser.PrintExprContext ctx) {
        Integer value = visit(ctx.expr()); 
        return value; 
    }
    
    @Override
    public Integer visitAssign(MathParser.AssignContext ctx) {
        String id = ctx.ID().getText(); 
        int value = visit(ctx.expr()); 
        memory.put(id, value);
        return value;
    }

    @Override
    public Integer visitBlank(MathParser.BlankContext ctx) {
        return super.visitBlank(ctx);
    }

    @Override
    public Integer visitParens(MathParser.ParensContext ctx) {
        return visit(ctx.expr()); 
    }

    @Override
    public Integer visitMulDiv(MathParser.MulDivContext ctx) {
        int left = visit(ctx.expr(0)); 
        int right = visit(ctx.expr(1));
        if ( ctx.op.getType() == MathParser.MUL ) return left * right;
        return left / right; 
    }
    
    @Override
    public Integer visitAddSub(MathParser.AddSubContext ctx) {
        // TODO Auto-generatedmethod stub
        int left = visit(ctx.expr(0)); 
        int right = visit(ctx.expr(1)); 
        if ( ctx.op.getType() == MathParser.ADD ) return left + right;
        return left - right; 
    }

    @Override
    public Integer visitId(MathParser.IdContext ctx) {
        String id = ctx.ID().getText();
        if ( memory.containsKey(id) ) return memory.get(id);
        return 0;
    }

    @Override
    public Integer visitInt(MathParser.IntContext ctx) {
        return Integer.valueOf(ctx.INT().getText());
    }
}

public class Math {
    public static void main(String[] args) {
        CharStream input = CharStreams.fromString("1*(6-3)/2");
        MathLexer lexer = new MathLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        MathParser parser = new MathParser(tokens);
        ParseTree tree = parser.prog(); // parse
        MathVisitorTest vt = new MathVisitorTest();
        Integer r = vt.visit(tree);
        System.out.println(r.toString());
    }
}

五、Spark-Sql之Antlr4

1.SqlBase.g4

SqlBase.g4是spark-sql的语法解析文件,所属模块为spark-catalyst,SqlBase.g4位于路径\spark-2.3\sql\catalyst\src\main\antlr4\org\apache\spark\sql\catalyst\parser\SqlBase.g4。查询的部分语法如下:

grammar SqlBase;
singleStatement
    : statement EOF
    ;
    statement
    : query                                                            #statementDefault
    ;
    
    query
    : ctes? queryNoWith
    ;
    
    queryNoWith
    : insertInto? queryTerm queryOrganization                                              #singleInsertQuery
    | fromClause multiInsertQueryBody+                                                     #multiInsertQuery
    ;
    
    queryTerm
    : queryPrimary                                                                         #queryTermDefault
    | left=queryTerm operator=(INTERSECT | UNION | EXCEPT | SETMINUS) setQuantifier? right=queryTerm  #setOperation
    ;
    
    queryPrimary
    : querySpecification                                                    #queryPrimaryDefault
    | TABLE tableIdentifier                                                 #table
    | inlineTable                                                           #inlineTableDefault1
    | '(' queryNoWith  ')'                                                  #subquery
    ;
    
    querySpecification
    : (((SELECT kind=TRANSFORM '(' namedExpressionSeq ')'
        | kind=MAP namedExpressionSeq
        | kind=REDUCE namedExpressionSeq))
       inRowFormat=rowFormat?
       (RECORDWRITER recordWriter=STRING)?
       USING script=STRING
       (AS (identifierSeq | colTypeList | ('(' (identifierSeq | colTypeList) ')')))?
       outRowFormat=rowFormat?
       (RECORDREADER recordReader=STRING)?
       fromClause?
       (WHERE where=booleanExpression)?)
    | ((kind=SELECT (hints+=hint)* setQuantifier? namedExpressionSeq fromClause?
       | fromClause (kind=SELECT setQuantifier? namedExpressionSeq)?)
       lateralView*
       (WHERE where=booleanExpression)?
       aggregation?
       (HAVING having=booleanExpression)?
       windows?)
    ;
2.访问者-AstBuilder

整个SQL解析相关的实现如下图:
在这里插入图片描述
AstBuilder实现了SqlBaseBaseVisitor,并实现了部分visitXXX方法

class AstBuilder(conf: SQLConf) extends SqlBaseBaseVisitor[AnyRef]{
  override def visitQuerySpecification(ctx: QuerySpecificationContext): LogicalPlan = withOrigin(ctx) {
      .......
  }
  override def visitSingleStatement(ctx: SingleStatementContext): LogicalPlan = withOrigin(ctx) {
    .......
  }

  override def visitSingleExpression(ctx: SingleExpressionContext): Expression = withOrigin(ctx) {
    .......
  }
}
3.Spark-SQL执行入口

SparkSession .sql(sqlText: String)是暴露给用户的方法,用于执行sql文本。

class SparkSession private({
  def sql(sqlText: String): DataFrame = {
    Dataset.ofRows(self, sessionState.sqlParser.parsePlan(sqlText))
  }
}

visitSingleStatement为根节点开始采用递归下降的方式遍历整个语法树,解析后返回的是LogicalPlan,后面我们将介绍逻辑计划(LogicalPlan)。

  override def parsePlan(sqlText: String): LogicalPlan = parse(sqlText) { parser =>
    astBuilder.visitSingleStatement(parser.singleStatement()) match {
      case plan: LogicalPlan => plan
      case _ =>
        val position = Origin(None, None)
        throw new ParseException(Option(sqlText), "Unsupported SQL statement", position, position)
    }
  }
  protected def parse[T](command: String)(toResult: SqlBaseParser => T): T = {
    logDebug(s"Parsing command: $command")
    val lexer = new SqlBaseLexer(new UpperCaseCharStream(CharStreams.fromString(command)))
    lexer.removeErrorListeners()
    lexer.addErrorListener(ParseErrorListener)
    val tokenStream = new CommonTokenStream(lexer)
    val parser = new SqlBaseParser(tokenStream)
    parser.addParseListener(PostProcessor)
    parser.removeErrorListeners()
    parser.addErrorListener(ParseErrorListener)
    try {
      try {
        // first, try parsing with potentially faster SLL mode
        parser.getInterpreter.setPredictionMode(PredictionMode.SLL)
        toResult(parser)
      }
      catch {
        ......
      }
    }
    catch {
      ......
    }
  }

逻辑计划(LogicalPlan)

一、TreeNode

TreeNode 是Catalyst的核心类,语法树的构建都是由一个个TreeNode组成。继承关系如下:
在这里插入图片描述
其核心方法的作用如下:
在这里插入图片描述
LogicalPlan是TreeNode 的子类,作为数据结构记录了对应逻辑算子树节点的基本信息和基本操作,包括输入输出和各种处理逻辑等。

二、QueryPlan

QueryPlan是LogicalPlan的直接父类,继承自TreeNode,其核心属性和方法可以分为6个部分:

1.输入输出

输入或输出属性,比如Project的output方法就返回所有查询的字段信息。

override def output: Seq[Attribute] = projectList.map(_.toAttribute)
2.基本属性

表示QueryPlan节点的一些基本信息,比如其中schema对应output输出属性的schema信息。

lazy val schema: StructType = StructType.fromAttributes(output)
3.字符串

打印QueryPlan树形结构信息。

4.规范化

QueryPlan的canonicalized直接赋值为当前的QueryPlan类,sameResult方法会利用canonicalized来判断两个QueryPlan的输出结果是否相同。

5.表达式操作

比如expressions会返回改节点所有表达式的列表,另外还有遍历表达式的方法transformExpressions等。

6.约束

可以推导的一种过滤条件,比如“a>1”,可以推出a不能为null。

三、Expression

Expression是SQL语句中的表达式,是指不需要执行引擎计算,而可以直接计算或处理的节点,包括Cast操作、Porjection操作、四则运算和逻辑操作符运算等等。

四、Commond

Commond是直接运行的命令,常见的有ShowCreateTableCommand(展示表)、CreateTableCommand(创建表)、AlterTableRenameCommand(修改表名字)等

trait Command extends LogicalPlan {
  override def output: Seq[Attribute] = Seq.empty
  override def children: Seq[LogicalPlan] = Seq.empty
}

五、LeafNode

LeafNode是叶子节点,是没有子节点的LogicalPlan,常见的有UnresolvedRelation(未解析的逻辑计划),所有经过antlr4解析后的都是UnresolvedRelation,不能被计算,有未绑定的属性和数据类型。

abstract class LeafNode extends LogicalPlan {
  override final def children: Seq[LogicalPlan] = Nil
  override def producedAttributes: AttributeSet = outputSet
  def computeStats(): Statistics = throw new UnsupportedOperationException
}

六、UnaryNode

UnaryNode有一个子节点,常见的有Filter(过滤)、Project(投影)、Window(窗口)等

abstract class UnaryNode extends LogicalPlan {
  def child: LogicalPlan
  override final def children: Seq[LogicalPlan] = child :: Nil
  protected def getAliasedConstraints(projectList: Seq[NamedExpression]): Set[Expression] = {
  ......
  }
  override protected def validConstraints: Set[Expression] = child.constraints
}

七、BinaryNode

BinaryNode有两个子节点,常见的有Join(关联)、Except(差集)、Intersect(交集)

abstract class BinaryNode extends LogicalPlan {
  def left: LogicalPlan
  def right: LogicalPlan
  override final def children: Seq[LogicalPlan] = Seq(left, right)
}

生成未解析逻辑计划(UnResolve LogicalPlan)

以查询为例子来说明生成未解析逻辑计划的过程,例如执行SQL为:

SELECT SUM(AGE)
 FROM
 (SELECT A.ID,
 A.NAME,
 CAST(B.AGE AS LONG) AS AGE 
 FROM 
 NAME A INNER JOIN AGE B 
 ON A.ID == B.ID) 
 WHERE AGE >20

通过antlr4解析后得到的抽象语法树如下图所示:
在这里插入图片描述
通过Asbuilder访问类对语法树进行访问,代码会执行

  override def visitQuerySpecification(
      ctx: QuerySpecificationContext): LogicalPlan = withOrigin(ctx) {
    val from = OneRowRelation().optional(ctx.fromClause) {
      visitFromClause(ctx.fromClause)
    }
    withQuerySpecification(ctx, from)
  }

生成UnResolve LogicalPlan的过程如图:
在这里插入图片描述
简化后的流程图为:
在这里插入图片描述
具体步骤如下:
(1)UnresolvedRelation:对应SQL语句的from的表名,访问FromClauseContext并递归向下访问,最终匹配到TableNameContext节点时,直接根据TableNameContext的信息生成UnresolvedRelation,构造名为from的LogicalPlan并返回。
(2)Join:对应SQL语句中的INNER JOIN语句,通过withJoinRelations最终构造Join(left, plan(join.right), joinType, condition)返回。包括左表、右表、关联类型、条件。
(3)Filter:对应SQL语句中的where语句,QuerySpecificationContext中包含了BooleanExpressionContext类型,Asbuilder会对改子树进行递归访问,此例中碰到ComparisonContext节点,生成GreaterThan表达式,然后生成expression并返回作为过滤条件,构造Filter(expression(ctx), plan)返回。
(4)Project:对应SQL语句中select对列值的选择操作,Asbuilder在访问中会获取NamedExpressionSeqContext,并对其所有子节点对应的表达式进行转换,生成Expression列表expressions,基于expressions构造Project(namedExpressions, withFilter)返回。
其中各节点中的Expression情况如下:
下表列出了构造Filter逻辑算子树节点中的condition表达式。根据ColumnReferenceContext节点信息生成UnresolvedAttribute表达式。

访问操作Expression
visitColumnReferenceUnresolvedAttribute(Seq(“age”))
visitIntegerLiteralLiteral(20, IntegerType)
visitComparisonGreaterThan(left, right)

下表列出了构造Project逻辑算子树节点中的condition表达式。

访问操作Expression
visitColumnReferenceUnresolvedAttribute(Seq(“a.id”,“a.name”,“cast(b.age as bigint)”))

树形结构如下图:
在这里插入图片描述

最终生成的未解析LogicalPlan为:

'Project ['sum('age) AS sum#9]
+- 'Filter ('age > 20)
   +- 'SubqueryAlias __auto_generated_subquery_name
      +- 'Project ['a.id, 'a.name, cast('b.age as bigint) AS age#8]
         +- 'Join Inner, ('a.id = 'b.id)
            :- 'SubqueryAlias a
            :  +- 'UnresolvedRelation `name`
            +- 'SubqueryAlias b
               +- 'UnresolvedRelation `age`

树形结构如下图所示:
在这里插入图片描述
至此,我们知道了Spark是如何将一个SQL语句通过Antlr4生成未解析的LogicalPlan的流程,这个LogicalPlan中的表名、函数名、字段名等都是未解析的,并没有绑定任何东西。接下来会进入Analyzer(分析)阶段,完成绑定操作,具体请参考下一篇文章。

参考资料
[1]: 《Spark SQL内部剖析》朱锋 张韶全 黄明 著
[2]: Spark SQL catalyst概述和SQL Parser的具体实现
[3]: 利用ANTLR4实现一个简单的四则运算计算器

<think>好的,我现在需要帮助用户解决在Spark SQL命令中正确导入所需包的问题。用户之前的问题已经涉及到了Hudi依赖包的导入,现在他们可能想更一般地了解如何在spark-sql命令中添加JAR包。 首先,我得回顾用户提供的引用内容。引用[1]提到在启动spark-sql时使用--jars参数加载MySQL连接器。引用[3]讨论了在Scala工程中导入必要的Spark SQL jar包。用户现在的问题是关于如何在spark-sql命令中导入包,这可能包括依赖管理和解决类路径问题。 用户的上一次回答中,详细讲解了如何导入Hudi相关的包,包括使用--jars和--packages参数。现在需要将这部分知识扩展到一般情况,而不仅仅是Hudi。需要确保用户明白不同的导入方式及其适用场景。 接下来,我需要整理常见的导入方法,比如通过--jars指定本地路径,或使用--packages从仓库下载。同时,要提到如何同时使用多个包,处理版本冲突,以及验证包是否正确加载。此外,用户可能遇到的问题,比如类找不到或者版本不兼容,也需要给出解决方案。 还要考虑用户可能的实际应用场景,比如连接数据库时需要JDBC驱动,处理不同数据格式可能需要额外的库。这时候,正确导入这些包就很重要了。例如,引用[4]提到时间格式问题,虽然不是直接相关,但说明用户可能在数据导入时遇到各种问题,正确的依赖管理可以避免一些错误。 另外,用户提到的引用[2]指出Spark SQL CLI类似于Hive窗口,可能需要配置Hive元数据服务,这时候也需要相关的依赖包。所以,在导入包的时候,可能还需要包含Hive相关的JAR,特别是当用户需要与Hive交互时。 最后,确保回答结构清晰,分点列出不同的方法,给出示例命令,并附上常见问题及解决方法。同时,生成的相关问题要覆盖用户可能遇到的后续疑问,比如验证包是否加载、处理依赖冲突等。</think>### 系统化解决方案:Spark-SQL 命令中导入包的3种核心方法 --- #### 一、基础导入方式 1. **`--jars` 参数加载本地JAR包** ```bash spark-sql --master local[2] \ --jars /path/to/mysql-connector.jar,/path/to/other.jar ``` - **适用场景**:本地已下载的依赖文件(如MySQL驱动、Hudi组件等)[^1] - **优势**:直接指定物理路径,无需网络下载 - **注意**:多个JAR包用逗号分隔,路径需为绝对路径 2. **`--packages` 从Maven仓库动态下载** ```bash spark-sql --packages "org.apache.hudi:hudi-spark-bundle_2.12:0.12.2,mysql:mysql-connector-java:8.0.28" ``` - **适用场景**:未提前下载的公共仓库依赖 - **自动解析**:自动下载传递依赖(transitive dependencies) - **版本格式**:`groupId:artifactId:version`(严格匹配) 3. **`--driver-class-path` 指定驱动类路径** ```bash spark-sql --driver-class-path /opt/hive/conf:/opt/hadoop/share/common ``` - **特殊用途**:加载配置文件(如Hive的`hive-site.xml`)或Hadoop原生库[^2] - **典型错误**:`ClassNotFoundException`时优先检查此参数 --- #### 二、组合使用技巧 | 场景描述 | 命令示例 | |--------------------------|-------------------------------------------------------------------------| | 同时加载本地JAR和远程依赖 | `--jars local.jar --packages "org.example:lib:1.0"` | | 指定私有仓库地址 | `--repositories https://nexus.example.com/repo/` | | 排除冲突依赖 | `--exclude-packages "com.google.guava:guava"` | --- #### 三、依赖加载验证方法 1. **日志确认** 检查启动日志中的以下关键信息: ```log Added JAR: file:/path/to/mysql-connector.jar at spark://driver Resolved dependency: org.apache.hudi#hudi-spark-bundle_2.12 added from repository central ``` 2. **运行时验证** ```sql SHOW JARS; -- 查看已加载的JAR包列表 CREATE TABLE test(...) USING hudi; -- 测试Hudi等需要特定依赖的功能 ``` 3. **错误诊断** - `NoClassDefFoundError`:依赖未正确加载或版本不匹配 - `MethodNotFound`:依赖版本冲突(如同时加载Spark/Hadoop不同版本) --- #### 四、典型问题解决方案 1. **Hive元数据服务集成问题** ```bash spark-sql --jars $HIVE_HOME/lib/*.jar \ --conf spark.sql.hive.metastore.jars=path://$HIVE_HOME/lib/ ``` - **现象**:建表成功但Hive查不到元数据[^2] - **根源**:缺少Hive Metastore连接器(如`hive-exec-*.jar`) 2. **时区/格式兼容问题** ```bash spark-sql --jars data-formats.jar \ --conf spark.sql.legacy.timeParserPolicy=LEGACY ``` - **关联引用[4]**:日期格式解析失败时需同时调整配置 3. **依赖冲突优先级** ```bash spark-sql --driver-class-path /higher/priority/path/lib.jar \ --jars /lower/priority/lib.jar ``` - **规则**:`--driver-class-path`优先级 > `--jars` > 默认类路径 --- #### 五、最佳实践建议 1. **标准化依赖管理** - 使用`dependencyManagement`统一版本(Maven/Gradle) - 制作自定义Docker镜像预置常用依赖 2. **生产环境部署** ```bash # 提交脚本示例 spark-sql \ --jars $(echo /lib/*.jar | tr ' ' ',') \ --packages "org.postgresql:postgresql:42.3.6" \ --conf spark.executor.extraClassPath=/lib/* ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值