Spark-SQL优化

Spark-SQL优化

前两篇文章Spark-SQL解析Spark-SQL绑定讲到了Spark-SQL通过Antlr4生成未解析的LogicalPlan。然后通过Catalog来绑定UnresolvedRelation和UnresolvedAttribute,生成解析后的LogicalPlan。本篇文章主要讲如何将解析后的逻辑计划按照写好的规则进行优化,能够指导编译器选择效率最高的执行计划。
传统数据库(例如Oracle)的优化器有两种:基于规则的优化器(Rule-Based Optimization,RBO)和基于代价的优化器(Cost-Based Optimization,CBO)
RBO使用的规则是根据经验形成的,只要按照这个规则去写SQL语句,无论数据表中的内容怎样、数据分布如何,都不会影响到执行计划。
CBO是根据实际数据分布和组织情况,评估每个计划的执行代价,从而选择代价最小的执行计划。
目前Spark的优化器都是基于RBO的,已经有数十条优化规则,例如谓词下推、常量折叠、投影裁剪等,这些规则是有效的,但是它对数据是不敏感的。导致的一个问题就是数据表中数据分布发生变化时,RBO是不感知的,基于RBO生成的执行计划不能确保是最优的。而CBO的重要作用就是能够根据实际数据分布估算出SQL语句,生成一组可能被使用的执行计划中代价最小的执行计划,从而提升性能。目前CBO主要的优化点是Join算法选择。举个简单例子,当两个表做Join操作,如果其中一张原本很大的表经过Filter操作之后结果集小于BroadCast的阈值,在没有CBO情况下是无法感知大表过滤后变小的情况,采用的是SortMergeJoin算法,涉及到大量Shuffle操作,很耗费性能;在有CBO的情况下是可以感知到结果集的变化,采用的是BroadcastHashJoin算法,会将过滤后的小表BroadCast到每个节点,转变为非Shuffle操作,从而大大提高性能。

优化(Optimizer)

和Analyzer类似,Optimizer也是继承自RuleExecutor[LogicalPlan],所有和前一篇的套路是一样的,在batches中定义好优化规则,然后通过RuleExecutor的execute(plan: TreeType)方法来顺序执行规则。Optimizer还有一个子类SparkOptimizer,SparkOptimizer里面定义了四个规则加一个外部规则,可支持用户自己扩展规则。
在这里插入图片描述
在这里插入图片描述
Optimizer的defaultBatches

def defaultBatches: Seq[Batch] = {
   
   
    val operatorOptimizationRuleSet =
      Seq(
        // Operator push down
        PushProjectionThroughUnion,
        ReorderJoin,
        EliminateOuterJoin,
       ...,
        // Operator combine
        CollapseRepartition,
        CollapseProject,
        CollapseWindow,
        ...)
     val operatorOptimizationBatch: Seq[Batch] = {
   
   
      val rulesWithoutInferFiltersFromConstraints =
        operatorOptimizationRuleSet.filterNot(_ == InferFiltersFromConstraints)
      Batch("Operator Optimization before Inferring Filters", fixedPoint,
        rulesWithoutInferFiltersFromConstraints: _*) ::
      Batch("Infer Filters", Once,
        InferFiltersFromConstraints) ::
      Batch("Operator Optimization after Inferring Filters", fixedPoint,
        rulesWithoutInferFiltersFromConstraints: _*) :: Nil
    }
  }

SparkOptimizer的defaultBatches

  override def defaultBatches: Seq[Batch] = (preOptimizationBatches ++ super.defaultBatches :+
    Batch("Optimize Metadata Only Query", Once, OptimizeMetadataOnlyQuery(catalog)) :+
    Batch("Extract Python UDFs", Once,
      Seq(ExtractPythonUDFFromAggregate, ExtractPythonUDFs): _*) :+
    Batch("Prune File Source Table Partitions", Once, PruneFileSourcePartitions) :+
    Batch("Parquet Schema Pruning", Once, ParquetSchemaPruning)) ++
    postHocOptimizationBatches :+
    Batch("User Provided Optimizers", fixedPoint, experimentalMethods.extraOptimizations: _*)

根据defaultBatches的注释,常用的规则可以大致分为下推操作、合并操作、常量折叠和长度削减等。

一、Push Down

Filter下推:优化器可以将谓词过滤下推到数据源,从而使物理执行跳过无关数据。其实就是把查询相关的条件下推到数据源进行提前的过滤操作,之所以这里说是查询相关的条件,而不直接说是where 后的条件,是因为sql语句中除了where后的有条件外,join时也有条件。
Project下推:通常,嵌套查询中我们需要查询的字段主要以外层的为准,外层即父节点,内层即子节点,Project下推即将父节点的投影下推到子节点中,可以过滤掉一些不必要的字段。

1. PushProjectionThroughUnion(Union的Project下推)

在多个语句进行UNION时,将Project下推到多个语句分别执行。

object PushProjectionThroughUnion extends Rule[LogicalPlan] with PredicateHelper {
   
   
  def apply(plan: LogicalPlan): LogicalPlan = plan transform {
   
   
    // Push down deterministic projection through UNION ALL
    case p @ Project(projectList, Union(children)) =>
      assert(children.nonEmpty)
      if (projectList.forall(_.deterministic)) {
   
   
        val newFirstChild = Project(projectList, children.head)
        val newOtherChildren = children.tail.map {
   
    child =>
          val rewrites = buildRewrites(children.head, child)
          Project(projectList.map(pushToRight(_, rewrites)), child)
        }
        Union(newFirstChild +: newOtherChildren)
      } else {
   
   
        p
      }
  }
}

以下SQL的分析后的执行计划和优化后的执行计划对比:

select api from 
(select api, bm_avg_visit_accumul from test
union all
select api1, bm_avg_visit_accumul1 from test1)
== Analyzed Logical Plan ==
api: string
Project [api#1]
+- SubqueryAlias `__auto_generated_subquery_name`
   +- Union
      :- Project [api#1, bm_avg_visit_accumul#5]
      :  +- SubqueryAlias `test`
      :     +- Relation[start#0,api#1,visit_accumul#2L,clientip_distinct_count#3L,max_batch_id#4L,bm_avg_visit_accumul#5] json
      +- Project [api1#13, bm_avg_visit_accumul1#17]
         +- SubqueryAlias `test1`
            +- Relation[start#12,api1#13,visit_accumul#14L,clientip_distinct_count#15L,max_batch_id#16L,bm_avg_visit_accumul1#17] json

== Optimized Logical Plan ==
Union
:- Project [api#1]
:  +- Relation[start#0,api#1,visit_accumul#2L,clientip_distinct_count#3L,max_batch_id#4L,bm_avg_visit_accumul#5] json
+- Project [api1#13]
   +- Relation[start#12,api1#13,visit_accumul#14L,clientip_distinct_count#15L,max_batch_id#16L,bm_avg_visit_accumul1#17] json
2. EliminateOuterJoin(消除外连接)

将一些和内连接逻辑等价的外连接转化为内连接,可以过滤很多不需要的记录。
如果谓词可以限制结果集使得所有空的行都被消除:
full outer -> inner:如果两边都有这样的谓语
left outer -> inner:如果右边有这样的谓语
right outer -> inner:如果左边有这样的谓语
full outer -> left outer:仅仅左边有这样的谓语
full outer -> right outer:仅仅右边有这样的谓语

object EliminateOuterJoin extends Rule[LogicalPlan] with PredicateHelper {
   
   

  /**
   * Returns whether the expression returns null or false when all inputs are nulls.
   */
  private def canFilterOutNull(e: Expression): Boolean = {
   
   
    if (!e.deterministic || SubqueryExpression.hasCorrelatedSubquery(e)) return false
    val attributes = e.references.toSeq
    val emptyRow = new GenericInternalRow(attributes.length)
    val boundE = BindReferences.bindReference(e, attributes)
    if (boundE.find(_.isInstanceOf[Unevaluable]).isDefined) return false
    val v = boundE.eval(emptyRow)
    v == null || v == false
  }

  private def buildNewJoinType(filter: Filter, join: Join): JoinType = {
   
   
    val conditions = splitConjunctivePredicates(filter.condition) ++ filter.constraints
    val leftConditions = conditions.filter(_.references.subsetOf(join.left.outputSet))
    val rightConditions = conditions.filter(_.references.subsetOf(join.right.outputSet))

    lazy val leftHasNonNullPredicate = leftConditions.exists(canFilterOutNull)
    lazy val rightHasNonNullPredicate = rightConditions.exists(canFilterOutNull)

    join.joinType match {
   
   
      case RightOuter if leftHasNonNullPredicate => Inner
      case LeftOuter if rightHasNonNullPredicate => Inner
      case FullOuter if leftHasNonNullPredicate && rightHasNonNullPredicate => Inner
      case FullOuter if leftHasNonNullPredicate => LeftOuter
      case FullOuter if rightHasNonNullPredicate => RightOuter
      case o => o
    }
  }

  def apply(plan: LogicalPlan): LogicalPlan = plan transform {
   
   
    case f @ Filter(condition, j @ Join(_, _, RightOuter | LeftOuter | FullOuter, _)) =
<think> 我们之前讨论了Spark提交作业的不同方式(spark-submit的各种模式),现在用户询问spark shell、spark-submit、spark-sql在提交作业方面的特点和区别。 根据用户提供的引用和之前的问题,我们需要总结这三个工具在Spark作业提交中的角色和区别。 1. spark-submit: - 引用[2]提到:spark-submit用来提交打成jar包的任务。它是一个提交Spark应用程序(通常是打包好的JAR包)到集群的命令行工具。 - 支持多种集群模式(Standalone, YARN, Mesos, Kubernetes)和部署模式(client/cluster)。 - 示例命令:提交一个计算Pi的Spark作业到YARN集群(cluster模式)[^2]。 2. spark-shell: - 引用[2]提到:spark-shell是一个简单的用来测试的交互式窗口。它是一个基于Scala的REPL(交互式解释环境),主要用于交互式数据处理和调试。 - 特点: - 自动初始化SparkContext(通常变量名为sc)和SparkSession(变量名为spark)。 - 适合小规模数据探索和快速原型开发,不能用于生产环境。 - 在启动时指定master(如--master yarn --deploy-mode client)[^2]。 3. spark-sql- 引用[4]提到:spark on hive & spark-sql on hive。它是一个用于执行Spark SQL查询的交互式命令行工具,类似于Hive的CLI。 - 特点: - 支持执行SQL语句,可以连接Hive Metastore(需要配置)[^4]。 - 通常用于执行SQL脚本或交互式查询,背后运行的是Spark SQL引擎。 - 也可以指定master(如--master yarn)来运行在集群上。 三者的主要区别: | 工具 | 用途 | 运行模式 | 适用场景 | |--------------|----------------------------------------------------------------------|--------------------|------------------------------| | **spark-submit** | 提交打包好的Spark应用程序(JAR包)到集群执行 | 支持所有集群模式 | 生产环境任务提交 | | **spark-shell** | 提供交互式Scala环境,用于探索性数据分析或调试(自动初始化Spark环境) | 通常使用client模式 | 开发/调试阶段 | | **spark-sql** | 提供SQL交互环境,执行SQL查询(支持Hive语法) | 通常使用client模式 | SQL查询、数据操作(替代Hive)| 详细说明: - **spark-submit**: - 是生产环境中最常用的提交方式。 - 支持细粒度资源控制(如--executor-memory, --executor-cores等)[^2]。 - 可以提交用Scala/Java/Python编写的Spark应用程序。 - **spark-shell**: - 启动后,用户可以直接在命令行中输入Scala代码,并立即看到结果。 - 在后台,它实际上启动了一个Spark应用(在指定的master上),但Driver在启动shell的机器上(client模式)。 - 不适合长时间运行的任务,因为一旦shell退出,任务就结束。 - **spark-sql**: - 启动后,可以执行SQL语句,也可以执行Spark SQL特有的命令(如SHOW TABLES)。 - 支持从文件系统(如HDFS)读取数据,也可以连接Hive Metastore[^4]。 - 可以通过`-f`参数执行SQL脚本文件。 示例: 1. 使用spark-submit提交作业(YARN cluster模式): ```bash spark-submit \ --class org.apache.spark.examples.SparkPi \ --master yarn \ --deploy-mode cluster \ --executor-memory 2g \ --num-executors 4 \ /path/to/examples.jar 100 ``` 2. 启动spark-shell(本地模式测试): ```bash spark-shell --master local[4] ``` 然后在shell中输入: ```scala val data = Array(1,2,3,4,5) val rdd = sc.parallelize(data) rdd.reduce(_ + _) ``` 3. 使用spark-sql查询Hive表: ```bash spark-sql --master yarn --conf spark.sql.hive.thriftServer.singleSession=true ``` 然后输入SQL: ```sql SELECT * FROM my_hive_table LIMIT 10; ``` 注意:spark-shell和spark-sql在提交时也可以指定资源参数(如--executor-memory),但通常只在交互式调试时使用。 总结: - 编写好的Spark应用程序(jar包)使用spark-submit提交。 - 需要交互式探索数据或调试代码用spark-shell。 - 需要执行SQL查询(尤其是Hive表)用spark-sql。 根据用户之前的问题,可能还涉及Hive集成(引用[1][4])和YARN模式(引用[5]),但这里主要关注三个工具的区别。 生成相关问题: </think> ### Spark Shell、Spark-Submit、Spark-SQL 在作业提交中的区别 以下是三个核心工具在Spark作业提交中的对比总结: | **工具** | **核心用途** | **运行模式** | **资源控制** | **适用场景** | |-----------------|--------------------------------------|----------------------------------|--------------------------|----------------------------------| | **spark-shell** | 交互式探索和调试 | `--master local[*]` (默认) | 不支持集群资源参数 | 开发调试、小规模数据探索 | | **spark-submit**| 提交打包的应用程序 | 支持所有模式(Local/YARN/K8s等) | 支持细粒度资源调控 | 生产环境作业、定时任务 | | **spark-sql** | 执行SQL/DDL操作 | 通常以`--master yarn-client`运行 | 有限支持资源参数 | Hive查询、数据仓库操作 | --- #### 1. **spark-shell:交互式探索工具** - **核心特点**: - REPL环境(Read-Eval-Print Loop),实时执行Scala/Python代码[^2] - **自动初始化**:启动时自动创建`SparkContext`(sc)和`SparkSession`(spark- **默认本地模式**:未指定`--master`时使用`local[*]`(本地所有核心)[^2] - **使用示例**: ```bash spark-shell \ --master yarn \ # 可切换至YARN模式 --executor-memory 4g # 资源控制有限 ``` ```scala // 交互式执行代码 val rdd = sc.parallelize(1 to 100) println(rdd.count()) ``` - **适用场景**: - 数据探索和原型验证 - 调试复杂转换逻辑 - 不适合生产环境长时间运行[^2] --- #### 2. **spark-submit:生产级作业提交** - **核心特点**: - 提交**预编译的JAR包**(Scala/Java)或Python脚本[^1][^2] - 完整集群资源控制: ```bash --executor-memory 8g \ --executor-cores 4 \ --num-executors 10 ``` - 支持**所有集群模式**: - YARN:`--master yarn --deploy-mode cluster`[^5] - Kubernetes:`--master k8s://...` - Standalone:`--master spark://...` - **使用示例**: ```bash spark-submit \ --class com.example.ETLJob \ --master yarn \ --deploy-mode cluster \ # Driver在YARN集群运行[^5] --queue production \ /path/to/your-app.jar ``` - **适用场景**: - 生产环境定时任务 - 大规模ETL流水线 - 需要资源隔离的作业[^2] --- #### 3. **spark-sqlSQL专用接口** - **核心特点**: - 专为SQL/DDL操作优化(兼容Hive语法)[^4] - 集成Hive Metastore: ```xml <property> <name>hive.execution.engine</name> <value>spark</value> <!-- 使用Spark引擎 --> </property> ``` - **执行方式**: - 交互式命令行:`spark-sql` - 脚本模式:`spark-sql -f script.sql` - **使用示例**: ```bash spark-sql \ --master yarn \ --conf spark.sql.shuffle.partitions=200 \ -e "SELECT * FROM hive_table LIMIT 10" # 直接执行SQL ``` - **适用场景**: - 替代Hive CLI执行查询[^4] - 数据仓库表管理(CREATE/ALTER) - SQL-centric工作流 --- ### 关键差异总结 1. **作业生命周期**: - `spark-shell`:会话结束时作业终止 - `spark-submit`:作业独立于客户端运行(尤其cluster模式) - `spark-sql`:SQL会话结束后释放资源 2. **元数据集成**: - `spark-sql`天然集成Hive Metastore[^4] - `spark-submit`需手动配置Hive支持 - `spark-shell`需显式启用Hive支持 3. **资源管理**: ```mermaid graph LR A[spark-shell] -->|仅基础配置| B(本地/YARN-client) C[spark-submit] -->|精细控制| D(所有集群模式) E[spark-sql] -->|中等控制| F(YARN-client为主) ``` > **最佳实践**: > - 开发阶段用`spark-shell`验证逻辑 > - 生产环境用`spark-submit`提交JAR包 > - Hive表操作首选`spark-sql`[^4] ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值