Spark SQL与Hive SQL解析执行流程

转载专用:
        读到了好文章,用于分享收藏,侵权删。
        转发自大佬:数据人生coding,https://yuanmore.blog.csdn.net/?type=blog
        转发自大佬:过往记忆,https://iteblog.blog.csdn.net/?type=blog
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://yuanmore.blog.csdn.net/article/details/124319891
本文链接:https://blog.csdn.net/wypblog/article/details/117719278?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166512377316800182172463%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=166512377316800182172463&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-2-117719278-null-null.nonecase&utm_term=Hive%20SQL%E5%BA%95%E5%B1%82%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E8%AF%A6%E7%BB%86%E5%89%96%E6%9E%90&spm=1018.2226.3001.4450

目录

Hive:

Apache Spark:

Hive SQL底层执行架构:

基本流程是:

Spark SQL底层执行架构:

Spark SQL整体执行流程图:

Catalyst的工作流程:

Catalyst 的两大优化:

Hive SQL 编译成 MapReduce 过程:

六个阶段的详细解析:


Hive:

Hive是数据仓库工具,再具体点就是一个 SQL 解析引擎,它不负责存储数据,也不负责计算数据,只负责解析 SQL,记录元数据。

Hive直接访问存储在 HDFS 中或者 HBase 中的文件,通过 MapReduce、Spark 或 Tez 执行查询。其实本质就是Hive实现了SQL on Hadoop,简化了MapReduce任务,只需写SQL就能进行大规模数据处理。

Hive的致命缺点是因为底层使用MapReduce做计算,查询延迟较高。

Apache Spark:

Apache Spark是用于大规模数据处理的统一分析引擎,基于内存计算,提高了在大数据环境下数据处理的实时性,同时保证了高容错性和高可伸缩性,允许用户将Spark部署在大量硬件之上,形成集群。

Hive SQL底层执行架构:

Hive 的底层执行架构图即Hive 的主要组件与 Hadoop 交互的过程:

上图在Hive这一侧,总共有五个组件:

  1. UI:用户界面。可看作我们提交SQL语句的命令行界面。
  2. DRIVER:驱动程序。接收查询的组件。该组件实现了会话句柄的概念。
  3. COMPILER:编译器。负责将 SQL 转化为平台可执行的执行计划。对不同的查询块和查询表达式进行语义分析,并最终借助表和从 metastore 查找的分区元数据来生成执行计划。
  4. METASTORE:元数据库。存储 Hive 中各种表和分区的所有结构信息。
  5. EXECUTION ENGINE:执行引擎。负责提交 COMPILER 阶段编译好的执行计划到不同的平台上。

基本流程是:

步骤1:UI 调用 DRIVER 的接口;
步骤2:DRIVER 为查询创建会话句柄,并将查询发送到 COMPILER(编译器)生成执行计划;
步骤3与步骤4:编译器从元数据存储中获取本次查询所需要的元数据,该元数据用于对查询树中的表达式进行类型检查,以及基于查询谓词修建分区;
步骤5:编译器生成的计划是分阶段的DAG,每个阶段要么是 Map/Reduce 作业,要么是一个元数据或者HDFS上的操作。将生成的计划发给 DRIVER(如果是Map/Reduce作业,该计划包括Map operator trees 和一个Reduce operator tree,执行引擎将会把这些作业发送给 MapReduce);
步骤6、6.1、6.2和6.3:执行引擎将这些阶段提交给适当的组件。在每个task(Mapper/Reducer)中,从HDFS文件中读取与表或中间输出相关联的数据,并通过相关算子树传递这些数据。最终这些数据通过序列化器写入到一个临时HDFS文件中(如果不需要Reduce阶段,则在Map中操作)。临时文件用于向计划中后面的Map/Reduce阶段提供数据。
步骤7、8和9:最终的临时文件将移动到表的位置,确保不读取脏数据(文件重命名在HDFS中是原子操作)。对于用户的查询,临时文件的内容由执行引擎直接从HDFS读取,然后通过Driver发送到UI。

Spark SQL底层执行架构:

从上图可看出,需要执行的SQL语句,要经过一个优化器(Catalyst),转化为RDD后交给集群执行。
SQL到RDD中间经过了一个Catalyst,它就是Spark SQL的核心,是针对Spark SQL语句执行过程中的查询优化框架,基于Scala函数式编程结构。
因此要了解Spark SQL的执行流程,那么理解Catalyst的工作流程是非常有必要的。

Spark SQL整体执行流程图:

Catalyst的工作流程:

一条SQL语句生成执行引擎可识别的程序,就离不开解析(Parser)优化(Optimizer)执行(Execution) 这三大过程。而Catalyst优化器在执行计划生成和优化的工作时候,它离不开自己内部的五大组件,如下所示:

  1. Parser模块:将SparkSql字符串解析为一个抽象语法树/AST。
  2. Analyzer模块:该模块会遍历整个AST,并对AST上的每个节点进行数据类型的绑定以及函数绑定,然后根据元数据信息Catalog对数据表中的字段进行解析。
  3. Optimizer模块:该模块是Catalyst的核心,主要分为RBO和CBO两种优化策略,其中RBO是基于规则优化,CBO是基于代价优化。
  4. SparkPlanner模块:优化后的逻辑执行计划OptimizedLogicalPlan依然是逻辑的,并不能被Spark系统理解,此时需要将OptimizedLogicalPlan转换成physical plan(物理计划) 。
  5. CostModel模块:主要根据过去的性能统计数据,选择最佳的物理执行计划。这个过程的优化就是CBO(基于代价优化)。

步骤1:Parser阶段,未解析的逻辑计划;
Parser简单说就是将SQL字符串切分成一个一个的Token,再根据一定语义规则解析成一颗语法树。Parser模块目前都是使用第三方类库ANTLR(Antlr是一种语言识别的工具,可以用来构造领域语言。使用Antlr构造特定的语言只需要编写一个语法文件,定义词法和语法替换规则即可,Antlr完成了词法分析、语法分析、语义分析、中间代码生成的过程。)进行实现的,包括Hive、Presto等也都是由ANTLR实现的。

在这个过程中,它会判断SQL语句是否符合规范,比如SELECT、FROM、WHERE等关键字是否写对。此阶段不会对表名,表字段进行检查。
步骤2:Analyzer阶段,解析后的逻辑计划;
通过解析后的逻辑计划基本有了骨架,此时需要基本的元数据信息来表达这些词素,最重要的元数据信息主要包括两部分:表的Scheme基本函数信息

表的Scheme:主要包括表的基本定义(列名、数据类型)、表的数据格式(Json、Text)、表的物理位置等;

基本函数:主要指类信息。
Analyzer会再次遍历整个语法树,对树上的每个节点进行数据类型绑定及函数绑定,比如people词素会根据元数据表信息解析为包含age、id、name三列的表,people.age会被解析为数据类型的int的变量,sum被解析为特定的聚合函数。 

步骤3:Optimizer模块:优化过的逻辑计划;
Optimizer优化模块是整个Catalyst的核心,上面提到优化器分为基于规则的优化(RBO)基于代价优化(CBO)两种。基于规则的优化策略实际上就是对语法树进行一次遍历,模式匹配能够满足特定规则的节点,在进行相应的等价转换。下面介绍三种常见的规则:谓词下推(Predicate Pushdown) 、常量累加(Constant Folding) 、列值裁剪(Column Pruning) 。
谓词下推(Predicate Pushdown):

上图左边是经过解析后的语法树,语法树中两个表先做JOIN,之后在使用age>10进行filter。join算子是一个非常耗时的算子,耗时多少一般取决于参与join的两个表的大小,如果能够减少参与join两表的大小,就可以大大降低join算子所需的时间。
谓词下推就是将过滤操作下推到join之前进行,之后再进行JOIN的时候,数据量将会得到显著的减少,join耗时必然降低。
常量累加(Constant Folding): 

常量累加就是比如计算x+(100+80)->x+180,虽然是一个很小的改动,但是意义巨大。如果没有进行优化的话,每一条结果都需要执行一次100+80的操作,然后再与结果相加。优化后就不需要再次执行100+80操作。
列值裁剪(Column Pruning):
列值裁剪是当用到一个表时,不需要扫描它的所有列值,而是扫描只需要的id,不需要的裁剪掉。这一优化一方面大幅度减少了网络、内存数据量消耗,另一方面对于列式存储数据库来说大大提高了扫描效率。
步骤4:SparkPlanner模块:转化为物理执行计划;
根据上面的步骤,逻辑执行计划已经得到了比较完善的优化,然而,逻辑执行计划依然没办法真正执行,他们只是逻辑上可行,实际上Spark并不知道如何去执行这个东西。比如JOIN是一个抽象概念,代表两个表根据相同的id进行合并,然而具体怎么实现合并,逻辑执行计划并没有说明。
此时就需要将逻辑执行计划转化为物理执行计划,也就是将逻辑上可行的执行计划变为Spark可以真正执行的计划。比如JOIN算子,Spark根据不同场景为该算子制定了不同的算法策略,有BroadcastHashJoin、ShuffleHashJoin和SortMergejoin等,物理执行计划实际上就是在这些具体实现中挑选一个耗时最小的算法实现,怎么挑选,下面简单说下:
实际上SparkPlanner对优化后的逻辑计划进行转换,是生成了多个可以执行的物理计划Physical Plan;
接着CBO(基于代价优化)优化策略会根据Cost Model算出每个Physical Plan的代价,并选取代价最小的 Physical Plan作为最终的Physical Plan。
以上2、3、4步骤合起来,就是Catalyst优化器。
步骤5:执行物理计划。
最后依据最优的物理执行计划,生成java字节码,将SQL转化为DAG,以RDD形式进行操作。


Catalyst 的两大优化:

RBO:基于规则的优化:
优化的点比如:谓词下推、列裁剪、常量累加等。
谓词下推案例:

SELECT 
    *
FROM table1 a
	JOIN table2 b ON a.id = b.id
WHERE a.age > 20
	AND b.cid = 1

上面的语句会自动优化为如下所示:

SELECT 
    *
FROM (
	SELECT 
        * FROM table1 WHERE age > 20
) a
	JOIN (
		SELECT * FROM table2 WHERE cid = 1
	) b
	ON a.id = b.id

在子查询阶段就提前将数据进行过滤,后期join的shuffle数据量就大大减少。
列裁剪案例:

SELECT 
    a.name,
    a.age,
    b.cid
FROM (
	SELECT 
        *
	FROM table1
	WHERE age > 20
) a
	JOIN (
		SELECT 
            *
		FROM table2
		WHERE cid = 1
	) b
	ON a.id = b.id

上面的语句会自动优化为如下所示:

SELECT 
    a.name,
    a.age,
    b.cid
FROM (
	SELECT name, age, id FROM table1 WHERE age > 20
) a
	JOIN (
		SELECT id, cid FROM table2 WHERE cid = 1
	) b
	ON a.id = b.id

提前将需要的列查询出来,其他不需要的列裁剪掉。
常量累加:

SELECT 
    1 + 1 AS id
FROM table1

上面的语句会自动优化为如下所示:

SELECT 
    2 AS id
FROM table1

提前将1+1计算成2,再赋给id列的每行,不用每次都去计算一次1+1。
CBO:基于代价的优化:
就是在SparkPlanner对优化后的逻辑计划生成了多个可以执行的物理计划Physical Plan之后,多个物理执行计划基于Cost Model选取最优的执行耗时最少的那个物理计划。

Hive SQL 编译成 MapReduce 过程:

编译 SQL 的任务是在COMPILER(编译器组件)中完成的。Hive将SQL转化为MapReduce任务,整个编译过程分为六个阶段:

  1. 词法、语法解析: Antlr 定义 SQL 的语法规则,完成 SQL 词法,语法解析,将 SQL 转化为抽象语法树 AST Tree;
  2. 语义解析: 遍历 AST Tree,抽象出查询的基本组成单元 QueryBlock;
  3. 生成逻辑执行计划: 遍历 QueryBlock,翻译为执行操作树 OperatorTree;
  4. 优化逻辑执行计划: 逻辑层优化器进行 OperatorTree 变换,合并 Operator,达到减少 MapReduce Job,减少数据传输及 shuffle 数据量;
  5. 生成物理执行计划: 遍历 OperatorTree,翻译为 MapReduce 任务;
  6. 优化物理执行计划: 物理层优化器进行 MapReduce 任务的变换,生成最终的执行计划。

六个阶段的详细解析:

为便于理解,这里拿一个简单的查询语句进行展示,对5月21号的地区维表进行查询:
select * from dim.dim_region where dt = '2021-05-21';
阶段一,词法、语法解析:
根据Antlr定义的sql语法规则,将相关sql进行词法、语法解析,转化为抽象语法树AST Tree:

ABSTRACT SYNTAX TREE:
TOK_QUERY
    TOK_FROM 
    TOK_TABREF
           TOK_TABNAME
               dim
                 dim_region
    TOK_INSERT
      TOK_DESTINATION
          TOK_DIR
              TOK_TMP_FILE
        TOK_SELECT
          TOK_SELEXPR
              TOK_ALLCOLREF
        TOK_WHERE
          =
              TOK_TABLE_OR_COL
                  dt
                    '2021-05-21'

阶段二,语义解析:
遍历AST Tree,抽象出查询的基本组成单元QueryBlock,AST Tree生成后由于其复杂度依旧较高,不便于翻译为Map Reduce程序,需要进行进一步抽象和结构化,形成QueryBlock。
QueryBlock是一条SQL最基本的组成单元,包括三个部分:输入源,计算过程,输出。简单来讲一个QueryBlock就是一个子查询。
QueryBlock的生成过程为一个递归过程,先序遍历 AST Tree ,遇到不同的 Token 节点(理解为特殊标记),保存到相应的属性中。
阶段三,生成逻辑执行计划:
遍历QueryBlock,翻译为执行操作树OperatorTree。Hive最终生成的MapReduce任务,Map阶段和Reduce阶段均由OperatorTree组成。
基本的操作符包括:

  • TableScanOperator
  • SelectOperator
  • FilterOperator
  • JoinOperator
  • GroupByOperator
  • ReduceSinkOperator

Operator在Map Reduce阶段之间的数据传递都是一个流式的过程。每一个Operator对一行数据完成操作后之后将数据传递给childOperator计算。
由于Join / GroupBy / OrderBy均需要在Reduce阶段完成,所以在生成相应操作的Operator之前都会先生成一个ReduceSinkOperator,将字段组合并序列化为Reduce Key/value, Partition Key。
阶段四,优化逻辑执行计划:
Hive中的逻辑查询优化可以大致分为以下几类:

  • 投影修剪
  • 推导传递谓词
  • 谓词下推
  • 将Select-Select,Filter-Filter合并为单个操作
  • 多路 Join
  • 查询重写以适应某些列值的Join倾斜

阶段五,生成物理执行计划:
生成物理执行计划即是将逻辑执行计划生成的OperatorTree转化为MapReduce Job的过程,主要分为下面几个阶段:

  1. 对输出表生成MoveTask。
  2. 从OperatorTree的其中一个根节点向下深度优先遍历。
  3. ReduceSinkOperator标示Map / Reduce的界限,多个Job间的界限。
  4. 遍历其他根节点,遇过碰到JoinOperator合并MapReduceTask。
  5. 生成StatTask更新元数据。
  6. 剪断Map与Reduce间的Operator的关系。

阶段六,优化物理执行计划:
Hive中的物理优化可以大致分为以下几类:

  • 分区修剪(Partition Pruning)
  • 基于分区和桶的扫描修剪(Scan pruning)
  • 如果查询基于抽样,则扫描修剪
  • 在某些情况下,在 map 端应用 Group By
  • 在 mapper 上执行 Join
  • 优化 Union,使Union只在 map 端执行
  • 在多路 Join 中,根据用户提示决定最后流哪个表
  • 删除不必要的 ReduceSinkOperators
  • 对于带有Limit子句的查询,减少需要为该表扫描的文件数
  • 对于带有Limit子句的查询,通过限制 ReduceSinkOperator 生成的内容来限制来自 mapper 的输出
  • 减少用户提交的SQL查询所需的Tez作业数量
  • 如果是简单的提取查询,避免使用MapReduce作业
  • 对于带有聚合的简单获取查询,执行不带 MapReduce 任务的聚合
  • 重写 Group By 查询使用索引表代替原来的表
  • 当表扫描之上的谓词是相等谓词且谓词中的列具有索引时,使用索引扫描

经过以上六个阶段,SQL 就被解析映射成了集群上的 MapReduce 任务。 

 

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值