SparkSparkSQL

第1章 SparkSQL概述

1.1  SparkSQL是什么 

1.2  Hive and SparkSQL

  SparkSQL的前身是Shark,给熟悉RDBMS但又不理解MapReduce的技术人员提供快速上手的工具。

  Hive是早期唯一运行在Hadoop上的SQL-on-Hadoop工具。但是MapReduce计算过程中大量的中间磁盘落地过程消耗了大量的I/O,降低的运行效率,为了提高SQL-on-Hadoop的效率,大量的SQL-on-Hadoop工具开始产生,其中表现较为突出的是:Drill、Impala、Shark。

  其中Shark是伯克利实验室Spark生态环境的组件之一,是基于Hive所开发的工具,它修改了下图所示的右下角的内存管理、物理计划、执行三个模块,并使之能运行在Spark引擎上。

  Shark的出现使得SQL-on-Hadoop的性能比Hive有了10-100倍的

  1)SparkSQL既支持SQL处理数据,又支持代码的方法(RDD)处理数据,更加灵活

  2)对接多种数据源,数据可以在MySQL,也可以在redis、Hbase、ES、Hive

  3)SparkSQL无缝支持HQL,若使用SparkSQL处理的数据是Hive管理起来的数据,则称为Spark On Hive;若使用的是SparkSQL来处理MySQL管理的数据,则称为Spark On MySQL

  4)HQL编程方式单一,处理的数据默认都是存储在HDFS中

  HiveOnSpark与SparkSQL的相同点与不同点:

    相同点:计算的数据都由Hive进行管理,使用Hive建表,将数据导入表中

    不同点:

      SparkSQL:

        1)可以写处理数据,也可以写代码处理数据

        2)只需要按照Spark,使用的是 SPARK_HOME/bin/spark-sql | spark-shell | spark-submit

        3)程序运行效率不同(sparksql更优,官方声称,是HiveOnSpark的十倍):HQL-->Spark的SQL解析器解析-->Spark的SQL优化器优化-->Spark App

      HiveOnSpark:

        1)只能写HQL

        2)本质上使用的是Hive,安装的是Hive,使用HIVE_HOME/bin/hive(cli),hiveserver2

        3)HQL-->Hive的SQL解析器解析-->Hive的SQL优化器优化-->Spark App

  但是,随着Spark的发展,对于野心勃勃的Spark团队来说,Shark对于Hive有太多依赖(如采用Hive的语法解析器、查询优化器等等),制约了Spark的One Stack Rule Them All的既定方针,制约了Spark各个组件的相互集成,所以提出了SparkSQL项目。SparkSQL抛弃原有Shark的代码,汲取了Shark的一些优点,如内存列存储(In-Memory Columnar Storage)、Hive兼容性等,重新开发了SparkSQL代码;由于摆脱了对Hive的依赖性,SparkSQL无论在数据兼容、性能优化、组件扩展方面都得到了极大的方便,真可谓“退一步,海阔天空”。

  数据兼容方面,SparkSQL不但兼容Hive,还可以从RDD、parquet文件、JSON文件中获取数据,未来版本甚至支持获取RDBMS数据以及cassandra等NOSQL数据;
  性能优化方面,除了采取In-Memory Columnar Storage、byte-code generation等优化技术外、将会引进Cost Model对查询进行动态评估、获取最佳物理计划等等;
  组件扩展方面,无论是SQL的语法解析器、分析器还是优化器都可以重新定义,进行扩展;

  2014年6月1日Shark项目和SparkSQL项目的主持人Reynold Xin宣布:停止对Shark的开发,团队将所有资源放SparkSQL项目上,至此,Shark的发展画上了句话。

  但也因此发展出两个支线:SparkSQL和Hive on Spark

  其中SparkSQL作为Spark生态的一员继续发展,而不再受限于Hive,只是兼容Hive;而Hive on Spark是一个Hive的发展计划,该计划将Spark作为Hive的底层引擎之一,也就是说,Hive将不再受限于一个引擎,可以采用Map-Reduce、Tez、Spark等引擎。

  对于开发人员来讲,SparkSQL可以简化RDD的开发,提高开发效率,且执行效率非常快,所以实际工作中,基本上采用的就是Spark SQL。Spark SQL为了简化RDD的开发,提高开发效率,提供了两个编程抽象:DataFrame 和 DataSet

1.3  SparkSQL特点

1.3.1  易整合

  无缝的整合了 SQL 查询和 Spark 编程

1.3.2  统一的数据访问

  使用相同的方式连接不同的数据源

1.3.3  兼容Hive

  在已有的仓库上直接运行 SQL 或者 HiveQL

1.3.4  标准数据连接

  通过 JDBC 或者 ODBC 来连接

1.4  DataFrame是什么(DF = RDD(数据) + schame(元数据) ,1.3版本推出,是弱类型

  在Spark中,DataFrame是一种以RDD为基础的分布式数据集,类似于传统数据库中的二维表格。DataFrame与RDD的主要区别在于,前者带有schema元信息,即DataFrame所表示的二维表数据集的每一列都带有名称和类型。这使得Spark SQL得以洞察更多的结构信息,从而对藏于DataFrame背后的数据源以及作用于DataFrame之上的变换进行了针对性的优化,最终达到大幅提升运行时效率的目标。反观RDD,由于无从得知所存数据元素的具体内部结构,Spark Core只能在stage层面进行简单、通用的流水线优化。
  同时,与Hive类似,DataFrame也支持嵌套数据类型(struct、array和map)。从 API 易用性的角度上看,DataFrame API提供的是一套高层的关系操作,比函数式的RDD API 要更加友好,门槛更低,下图可以直观地体现了DataFrame和RDD的区别:

  左侧的RDD[Person]虽然以Person为类型参数,但Spark框架本身不了解Person类的内部结构。而右侧的DataFrame却提供了详细的结构信息,使得 Spark SQL 可以清楚地知道该数据集中包含哪些列,每列的名称和类型各是什么。
  DataFrame是为数据提供了Schema的视图。可以把它当做数据库中的一张表来对待
  DataFrame也是懒执行的,但性能上比RDD要高,主要原因是优化了执行计划,即查询计划通过Spark catalyst optimiser进行优化。比如下面一个例子:

users.join(events,users("id") === events("uid")).filter(events("date") > "2021-06-06")

  为了说明查询优化,我们来看上图展示的人口数据分析的示例。图中构造了两个DataFrame,将它们join之后又做了一次filter操作。如果原封不动地执行这个执行计划,最终的执行效率是不高的。因为join是一个代价较大的操作,也可能会产生一个较大的数据集。如果我们能将filter下推到 join下方,先对DataFrame进行过滤,再join过滤后的较小的结果集,便可以有效缩短执行时间。而Spark SQL的查询优化器正是这样做的。简而言之,逻辑查询计划优化就是一个利用基于关系代数的等价变换,将高成本的操作替换为低成本操作的过程。 

1.5  DataSet是什么(Dataframe= DataSet[Row],是强类型,Row是一行数据

  DataSet是分布式数据集合,DataSetSpark 1.6中添加的一个新抽象,是DataFrame的一个扩展。它提供了RDD的优势(强类型,使用强大的lambda函数的能力)以及Spark SQL优化执行引擎的优点。DataSet也可以使用功能性的转换(操作mapflatMapfilter等等)

  1)DataSet是DataFrame API的一个扩展,是SparkSQL最新的数据抽象
  2)用户友好的API风格,既具有类型安全检查也具有DataFrame的查询优化特性;
  3)用样例类来定义DataSet中数据的结构信息,样例类中每个属性的名称直接映射到DataSet中的字段名称;
  4)DataSet是强类型的,比如可以有DataSet[Car],DataSet[Person]
  5)DataFrame是DataSet的特列,DataFrame=DataSet[Row] ,所以可以通过as方法将DataFrame转换为DataSet。Row是一个类型,跟Car、Person这些的类型一样,所有的表结构信息都用Row来表示(获取数据时需要指定顺序)

第2章  SparkSQL核心编程

  学习如何使用 Spark SQL所提供的 DataFrameDataSet模型进行编程.以及了解它们之间的关系和转换,关于具体的SQL书写不是我们的重点

2.1 新的起点

  Spark Core中,如果想要执行应用程序,需要首先构建上下文环境对象SparkContextSpark SQL其实可以理解为对Spark Core的一种封装,不仅仅在模型上进行了封装,上下文环境对象也进行了封装。

  在老的版本中,SparkSQL提供两种SQL查询起始点:一个叫SQLContext,用于Spark自己提供的SQL查询;一个叫HiveContext,用于连接Hive的查询。

  SparkSessionSpark最新的SQL查询起始点,实质上是SQLContextHiveContext的组合,所以在SQLContexHiveContext上可用的APISparkSession上同样是可以使用的。SparkSession内部封装了SparkContext,所以计算实际上是由sparkContext完成的。当我们使用 spark-shell 的时候, spark 会自动的创建一个叫做sparkSparkSession对象, 就像我们以前可以自动获取到一个sc来表示SparkContext对象一样

2.2 DataFrame

  Spark SQLDataFrame API 允许我们使用 DataFrame 而不用必须去注册临时表或者生成 SQL 表达式。DataFrame API 既有 transformation操作也有action操作

2.2.1 创建DataFrame

  在Spark SQLSparkSession是创建DataFrame和执行SQL的入口,创建DataFrame有三种方式:通过Spark的数据源进行创建;从一个存在的RDD进行转换;还可以从Hive Table进行查询返回

  1) Spark数据源进行创建

    (1)查看Spark支持创建文件的数据源格式

    (2)在HDFS中的/spark/input目录中创建user.json文件

{"username":"zhangsan","age":20}

    (3)读取json文件创建DataFrame

val df = spark.read.json("/spark/input/user.json");

      注意:如果从内存中获取数据,spark可以知道数据类型具体是什么如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换

    (4)展示结果

df.show();

  2) RDD进行转换(在后续章节中讨论)

  3) Hive Table进行查询返回(在后续章节中讨论)

2.2.2 SQL语法

  SQL语法风格是指我们查询数据的时候使用SQL语句来查询,这种风格的查询必须要有临时视图或者全局视图来辅助

  1) 读取JSON文件创建DataFrame

val df = spark.read.json("/spark/input/user.json");

  2) DataFrame创建一个临时表

df.createOrReplaceTempView("people");

  3) 通过SQL语句实现查询全表

val dfSQL = spark.sql("select * from people");

  4) 结果展示(普通临时表是Session范围内的,如果想应用范围内有效,可以使用全局临时表。使用全局临时表时需要全路径访问,如:global_temp.people)

dfSQL.show();

  5) 对于DataFrame创建一个全局表

//数据准备
{"username": "zhangsan","age": 25}
{"username": "lisi","age": 18}
{"username": "wangwu","age": 45}
df.createGlobalTempView("people")

  6) 通过SQL语句实现查询全表

spark.sql("SELECT * FROM global_temp.people").show()

spark.newSession().sql("SELECT * FROM global_temp.people").show()

2.2.3 DSL语法

  DataFrame提供一个特定领域语言(domain-specific language, DSL)去管理结构化的数据可以在 Scala, Java, Python R 中使用 DSL,使用 DSL 语法风格不必去创建临时视图了

  1) 创建一个DataFrame

val df = spark.read.json("/spark/input/user.json");

  2) 查看DataFrameSchema信息

df.printSchema

  3) 只查看"username"列数据

df.select("username").show()

  4) 查看"username"列数据以及"age+1"数据(涉及到运算的时候, 每列都必须使用$;或者采用引号表达式:单引号+字段名)

df.select($"username",$"age" + 1).show()

df.select('username,'age + 1).show()

df.select('username,'age + 1 as "newage").show()

  5) 查看"age"大于"30"的数据(往user.json文件添加数据)

{"username":"lisi","age":25}
{"username":"wangwu","age":45}
//重新读取文件
val df = spark.read.json("/spark/input/user.json");
df.filter($"age" > 30).show()

  6) 按照"age"分组,查看数据条数

df.groupBy("age").count.show()

2.2.4 RDD转换为DataFrame

  在IDEA中开发程序时,如果需要RDDDF或者DS之间互相操作,那么需要引入 import spark.implicits._  ,这里的spark不是Scala中的包名,而是创建的sparkSession对象的变量名称,所以必须先创建SparkSession对象再导入。这里的spark对象不能使用var声明,因为Scala只支持val修饰的对象的引入

  spark-shell中无需导入,自动完成此操作

val rdd = sc.textFile("/spark/input/1.txt");
rdd.toDF("name").show()

  实际开发中,一般通过样例类将RDD转换为DataFrame

case class User(name:String, age:Int)
sc.makeRDD(List(("zhangsan",30), ("lisi",40))).map(t=>User(t._1, t._2)).toDF.show()

2.2.5 DataFrame转换为RDD

  DataFrame其实就是对RDD的封装,所以可以直接获取内部的RDD

val df = sc.makeRDD(List(("zhangsan",30), ("lisi",40))).map(t=>User(t._1, t._2)).toDF
val rdd = df.rdd
val array = rdd.collect

   此时得到的RDD存储类型为Row

array(0)

array(0)(0)

array(0).getAs[String]("name")

2.3 DataSet

  DataSet是具有强类型的数据集合,需要提供对应的类型信息。

2.3.1 创建DataSet

  1) 使用样例类序列创建DataSet

case class Person(name: String, age: Long)
val caseClassDS = Seq(Person("zhangsan",2)).toDS()

caseClassDS.show

  2) 使用基本类型的序列创建DataSet(在实际使用的时候,很少用到把序列转换成DataSet,更多的是通过RDD来得到DataSet)

val ds = Seq(1,2,3,4,5).toDS

ds.show

2.3.2 RDD转换为DataSet

  SparkSQL能够自动将包含有case类的RDD转换成DataSetcase类定义了table的结构,case类属性通过反射变成了表的列名。Case类可以包含诸如Seq或者Array等复杂的结构

case class User(name:String, age:Int)
sc.makeRDD(List(("zhangsan",30), ("lisi",49))).map(t=>User(t._1, t._2)).toDS

2.3.3 DataSet转换为RDD

  DataSet其实是对RDD的封装,所以可以直接获取内部的RDD

case class User(name:String, age:Int)
val result = sc.makeRDD(List(("zhangsan",30), ("lisi",49))).map(t=>User(t._1, t._2)).toDS

val rdd = result.rdd

rdd.collect

2.4 DataFrameDataSet转换

  DataFrame其实是DataSet的特例,所以它们之间是可以互相转换的。

  1)DataFrame转换为DataSet

case class User(name:String, age:Int)
val df = sc.makeRDD(List(("zhangsan",30), ("lisi",49))).toDF("name","age")

val ds = df.as[User]

  2)DataSet转换为DataFrame

val df = ds.toDF

2.5 RDDDataFrameDataSet三者的关系

  在SparkSQLSpark为我们提供了两个新的抽象,分别是DataFrameDataSet。他们和RDD有什么区别呢?首先从版本的产生上来看:

    1)Spark1.0 => RDD

    2)Spark1.3 => DataFrame

    3)Spark1.6 => Dataset

  如果同样的数据都给到这三个数据结构,他们分别计算之后,都会给出相同的结果。不同是的他们的执行效率和执行方式。在后期的Spark版本中,DataSet有可能会逐步取代RDDDataFrame成为唯一的API接口。

2.5.1 三者的共性

  1)RDDDataFrameDataSet全都是spark平台下的分布式弹性数据集,为处理超大型数据提供便利;

  2)三者都有惰性机制,在进行创建、转换,如map方法时,不会立即执行,只有在遇到Actionforeach时,三者才会开始遍历运算;

  3)三者有许多共同的函数,如filter,排序等;

  4)在对DataFrameDataset进行操作许多操作都需要这个包:import spark.implicits._(在创建好SparkSession对象后尽量直接导入)

  5)三者都会根据 Spark 的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出

  6)三者都有partition的概念

  7)DataFrameDataSet均可使用模式匹配获取各个字段的值和类型

2.5.2 三者的区别

  1) RDD

    (1)RDD一般和spark mlib同时使用

    (2)RDD不支持sparksql操作

  2) DataFrame

    (1)RDDDataset不同,DataFrame每一行的类型固定为Row,每一列的值没法直接访问,只有通过解析才能获取各个字段的值

    (2)DataFrameDataSet一般不与 spark mlib 同时使用

    (3)DataFrameDataSet均支持 SparkSQL 的操作,比如selectgroupby之类,还能注册临时表/视窗,进行 sql 语句操作

    (4)DataFrameDataSet支持一些特别方便的保存方式,比如保存成csv,可以带上表头,这样每一列的字段名一目了然

  3) DataSet

    (1)DatasetDataFrame拥有完全相同的成员函数,区别只是每一行的数据类型不同。 DataFrame其实就是DataSet的一个特例  type DataFrame = Dataset[Row]

    (2)DataFrame也可以叫Dataset[Row],每一行的类型是Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用上面提到的getAS方法或者共性中的第七条提到的模式匹配拿出特定字段。而Dataset中,每一行是什么类型是不一定的,在自定义了case class之后可以很自由的获得每一行的信息

2.5.3 三者的互相转换

2.6 IDEA开发SparkSQL

2.6.1 添加依赖

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-sql_2.12</artifactId>
    <version>3.0.0</version>
</dependency>

2.6.2 代码实现

package com.yuange.spark.day07

import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}
import org.apache.spark.{SparkConf, SparkContext}

case class User(id:Int,name:String,age:Int)

/**
 * @作者:袁哥
 * @时间:2021/6/7 19:19
 */
object TestSparkSQL {
  def main(args: Array[String]): Unit = {
    //创建上下文,配置环境变量
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("TestSparkSQL")
    //创建SparkSession对象
    val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
    //RDD=>DataFrame=>DataSet转换需要引入隐式转换规则,否则无法转换
    //spark不是包名,是上下文环境对象名
    import spark.implicits._

    //读取json文件 创建DataFrame
    //{"username": "zhangsan","age": 25}
    //{"username": "lisi","age": 18}
    //{"username": "wangwu","age": 45}
    val df: DataFrame = spark.read.json("datas/testSparkSQL.json")
    df.show()

    //SQL风格语法
    df.createOrReplaceTempView("user")
    spark.sql("select avg(age) from user").show()

    //DSL风格语法
    df.select("username","age").show()

    //创建一个RDD
    val rdd2: RDD[(Int,String,Int)] = spark.sparkContext.makeRDD(List((1,"zhangsan",30),(2,"lisi",28),(3,"wangwu",20)))
    //rdd-->DataFrame
    val df2: DataFrame = rdd2.toDF("id","name","age")
    df2.show()
    //DataFrame-->DataSet
    val ds2: Dataset[User] = df2.as[User]
    ds2.show()
    //DataSet-->DataFrame
    val df3: DataFrame = ds2.toDF()
    df3.show()
    //DataFrame-->rdd:返回的RDD类型为Row,里面提供的getXXX方法可以获取字段值,类似jdbc处理结果集,但是索引从0开始
    val rdd3: RDD[Row] = df3.rdd
    rdd3.foreach(println)

    //rdd-->DataSet
    rdd2.map{
      case (id,name,age) => User(id,name,age)
    }.toDS().show()
    //DataSet-->rdd
    ds2.rdd.foreach(println)
  }
}

2.7 用户自定义函数

  用户可以通过spark.udf功能添加自定义函数,实现自定义功能。

2.7.1 UDF

  1) 创建DataFrame

val df = spark.read.json("/spark/input/user.json");

  2) 注册UDF

spark.udf.register("addName",(x: String) => "Name:" + x)

  3) 创建临时表

df.createOrReplaceTempView("people")

  4) 应用UDF

spark.sql("Select addName(username),age from people").show()

2.7.2 UDAF

  强类型的Dataset和弱类型的DataFrame都提供了相关的聚合函数, 如 count()countDistinct()avg()max()min()。除此之外,用户可以设定自己的自定义聚合函数。通过继承UserDefinedAggregateFunction来实现用户自定义聚合函数。

  需求(一个需求可以采用很多种不同的方法实现需求):计算平均工资

  1) 实现方式 - RDD

package com.yuange.spark.day07

import org.apache.spark.{SparkConf, SparkContext}

/**
 * @作者:袁哥
 * @时间:2021/6/7 20:25
 */
object TestSparkSQLTwo {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setAppName("app").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)
    val res: (Int, Int) = sc.makeRDD(List(("zhangsan", 20), ("lisi", 30), ("wangw", 40))).map {
      case (name, age) => {
        (age, 1)
      }
    }.reduce {
      (t1, t2) => {
        (t1._1 + t2._1, t1._2 + t2._2)
      }
    }
    println(res._1/res._2)
    // 关闭连接
    sc.stop()
  }
}

  2) 实现方式 - 累加器

package com.yuange.spark.day07

import org.apache.spark.util.AccumulatorV2

/**
 * @作者:袁哥
 * @时间:2021/6/7 20:27
 */
class MyAccumulatorV2 extends AccumulatorV2[Int,Int]{
  var sum: Int = 0
  var count: Int = 0

  override def isZero: Boolean = {
    return sum == 0 && count == 0
  }

  override def copy(): AccumulatorV2[Int, Int] = {
    val newMyAccumulatorV2 = new MyAccumulatorV2
    newMyAccumulatorV2.sum = this.sum
    newMyAccumulatorV2.count = this.count
    newMyAccumulatorV2
  }

  override def reset(): Unit = {
    sum = 0
    count = 0
  }

  override def add(v: Int): Unit = {
    sum += v
    count += 1
  }

  override def merge(other: AccumulatorV2[Int, Int]): Unit = {
    other match {
      case o: MyAccumulatorV2 => {
        sum += o.sum
        count += o.count
      }
      case _ => {}
    }
  }

  override def value: Int = sum/count
}

  3) 实现方式 - UDAF - 弱类型

package com.yuange.spark.day07

import org.apache.spark
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.sql.{DataFrame, Row, SparkSession}
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types.{DataType, DoubleType, IntegerType, LongType, StructField, StructType}

/**
 * @作者:袁哥
 * @时间:2021/6/7 20:39
 */
/*
定义类继承UserDefinedAggregateFunction,并重写其中方法
*/
class MyAveragUDAF extends UserDefinedAggregateFunction {

  // 聚合函数输入参数的数据类型
  def inputSchema: StructType = StructType(Array(StructField("age",IntegerType)))

  // 聚合函数缓冲区中值的数据类型(age,count)
  def bufferSchema: StructType = {
    StructType(Array(StructField("sum",LongType),StructField("count",LongType)))
  }

  // 函数返回值的数据类型
  def dataType: DataType = DoubleType

  // 稳定性:对于相同的输入是否一直返回相同的输出。
  def deterministic: Boolean = true

  // 函数缓冲区初始化
  def initialize(buffer: MutableAggregationBuffer): Unit = {
    // 存年龄的总和
    buffer(0) = 0L
    // 存年龄的个数
    buffer(1) = 0L
  }

  // 更新缓冲区中的数据
  def update(buffer: MutableAggregationBuffer,input: Row): Unit = {
    if (!input.isNullAt(0)) {
      buffer(0) = buffer.getLong(0) + input.getInt(0)
      buffer(1) = buffer.getLong(1) + 1
    }
  }

  // 合并缓冲区
  def merge(buffer1: MutableAggregationBuffer,buffer2: Row): Unit = {
    buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0)
    buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)
  }

  // 计算最终结果
  def evaluate(buffer: Row): Double = buffer.getLong(0).toDouble / buffer.getLong(1)
}

object MyAveragUDAF{
  def main(args: Array[String]): Unit = {
    //创建上下文,配置环境变量
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("TestSparkSQL")
    //创建SparkSession对象
    val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
    //RDD=>DataFrame=>DataSet转换需要引入隐式转换规则,否则无法转换
    //spark不是包名,是上下文环境对象名
    import spark.implicits._

    //创建聚合函数
    var myAverage = new MyAveragUDAF
    //在spark中注册聚合函数
    spark.udf.register("avgAge",myAverage)

    val df: DataFrame = spark.read.json("datas/testSparkSQL.json")
    //SQL风格语法
    df.createOrReplaceTempView("user")

    spark.sql("select avgAge(age) from user").show()
  }
}

  4) 实现方式 - UDAF - 强类型

package com.yuange.spark.day07

import org.apache.spark.SparkConf
import org.apache.spark.sql.{DataFrame, Dataset, Encoder, Encoders, SparkSession, TypedColumn}
import org.apache.spark.sql.expressions.Aggregator

//输入数据类型
case class User2(username:String,age:Long)
//缓存类型
case class AgeBuffer(var sum:Long,var count:Long)

/**
 * @作者:袁哥
 * @时间:2021/6/7 20:50
 * 定义类继承org.apache.spark.sql.expressions.Aggregator
 * 重写类中的方法
 */
class MyAveragUDAF2 extends Aggregator[User2,AgeBuffer,Double]{
  override def zero: AgeBuffer = {
    AgeBuffer(0L,0L)
  }

  override def reduce(b: AgeBuffer, a: User2): AgeBuffer = {
    b.sum = b.sum + a.age
    b.count = b.count + 1
    b
  }

  override def merge(b1: AgeBuffer, b2: AgeBuffer): AgeBuffer = {
    b1.sum = b1.sum + b2.sum
    b1.count = b1.count + b2.count
    b1
  }

  override def finish(buff: AgeBuffer): Double = {
    buff.sum.toDouble/buff.count
  }
  //DataSet默认额编解码器,用于序列化,固定写法
  //自定义类型就是product自带类型根据类型选择
  override def bufferEncoder: Encoder[AgeBuffer] = {
    Encoders.product
  }

  override def outputEncoder: Encoder[Double] = {
    Encoders.scalaDouble
  }

}

object MyAveragUDAF2{
  def main(args: Array[String]): Unit = {
    //创建上下文,配置环境变量
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("TestSparkSQL")
    //创建SparkSession对象
    val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
    //RDD=>DataFrame=>DataSet转换需要引入隐式转换规则,否则无法转换
    //spark不是包名,是上下文环境对象名
    import spark.implicits._

    val df: DataFrame = spark.read.json("datas/testSparkSQL.json")

    //封装为DataSet
    val ds: Dataset[User2] = df.as[User2]

    //创建聚合函数
    var myAgeUdaf2 = new MyAveragUDAF2
    //将聚合函数转换为查询的列
    val col: TypedColumn[User2, Double] = myAgeUdaf2.toColumn

    //查询
    ds.select(col).show()
  }
}

2.8 数据的加载和保存

2.8.1 通用的加载和保存方式

  SparkSQL提供了通用的保存数据和数据加载的方式。这里的通用指的是使用相同的API,根据不同的参数读取和保存不同格式的数据,SparkSQL默认读取和保存的文件格式为parquet

  1) 加载数据(spark.read.load 是加载数据的通用方法)

spark.read.

    如果读取不同格式的数据,可以对不同的数据格式进行设定

spark.read.format("…")[.option("…")].load("…")

    (1)format("…"):指定加载的数据类型,包括"csv""jdbc""json""orc""parquet""textFile"

    (2)load("…"):在"csv""jdbc""json""orc""parquet""textFile"格式下需要传入加载数据的路径。

    (3)option("…"):在"jdbc"格式下需要传入JDBC相应参数,urluserpassworddbtable

    我们前面都是使用read API 先把文件加载到 DataFrame然后再查询,其实,我们也可以直接在文件上进行查询:  文件格式.`文件路径`

spark.sql("select * from json.`/spark/input/user.json`").show

  2) 保存数据(df.write.save 是保存数据的通用方法)

df.write.

    如果保存不同格式的数据,可以对不同的数据格式进行设定

df.write.format("…")[.option("…")].save("…")

    (1)format("…"):指定保存的数据类型,包括"csv""jdbc""json""orc""parquet""textFile"

    (2)save ("…"):在"csv""orc""parquet""textFile"格式下需要传入保存数据的路径。

    (3)option("…"):在"jdbc"格式下需要传入JDBC相应参数,urluserpassworddbtable

    保存操作可以使用 SaveMode, 用来指明如何处理数据,使用mode()方法来设置(这些 SaveMode 都是没有加锁的, 也不是原子操作),SaveMode是一个枚举类,其中的常量包括:

Scala/Java

Any Language

Meaning

SaveMode.ErrorIfExists(default)

"error"(default)

如果文件已经存在则抛出异常

SaveMode.Append

"append"

如果文件已经存在则追加

SaveMode.Overwrite

"overwrite"

如果文件已经存在则覆盖

SaveMode.Ignore

"ignore"

如果文件已经存在则忽略

df.write.mode("append").json("/spark/output")

2.8.2 Parquet

  Spark SQL的默认数据源为Parquet格式。Parquet是一种能够有效存储嵌套数据的列式存储格式

  数据源为Parquet文件时,Spark SQL可以方便的执行所有的操作,不需要使用format。修改配置项spark.sql.sources.default,可修改默认数据源格式。

  1) 加载数据(先将/opt/module/spark-yarn/examples/src/main/resources路径下的users.parquet上传至/spark/input)

hadoop fs -put /opt/module/spark-yarn/examples/src/main/resources/users.parquet /spark/input/
val df = spark.read.load("/spark/input/users.parquet")

df.show()

  2) 保存数据

var df = spark.read.json("/spark/input/user.json")
df.write.mode("append").save("/spark/output/save/")

2.8.3 JSON

  Spark SQL 能够自动推测JSON数据集的结构,并将它加载为一个Dataset[Row]. 可以通过SparkSession.read.json()去加载JSON 文件。

  Spark读取的JSON文件不是传统的JSON文件,每一行都应该是一个JSON串,格式如下:

{"name":"Michael"}
{"name":"Andy", "age":30}
[{"name":"Justin", "age":19},{"name":"Justin", "age":19}]

  1)导入隐式转换

import spark.implicits._

  2)加载JSON文件

val path = "/spark/input/user.json"
val df= spark.read.json(path)

  3)创建临时表

df.createOrReplaceTempView("user")

  4)数据查询

spark.sql("SELECT username FROM user WHERE age BETWEEN 13 AND 19").show()

2.8.4 CSV

  Spark SQL可以配置CSV文件的列表信息,读取CSV文件,CSV文件的第一行设置为数据列

spark.read.format("csv").option("sep", ";").option("inferSchema", "true").option("header", "true").load("data/user.csv")

2.8.5 MySQL

  Spark SQL可以通过JDBC从关系型数据库中读取数据的方式创建DataFrame,通过对DataFrame一系列的计算后,还可以将数据再写回关系型数据库中。如果使用spark-shell操作,可在启动shell时指定相关的数据库驱动路径或者将相关的数据库驱动放到spark的类路径下。

bin/spark-shell --jars jars/mysql-connector-java-5.1.27-bin.jar

  这里只演示在Idea中通过JDBCMysql进行操作

  1)导入依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.27</version>
</dependency>

  2)读取数据

package com.yuange.spark.day08

import java.util.Properties

import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession

/**
 * @作者:袁哥
 * @时间:2021/6/7 21:30
 */
object TestMysql {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("TestMysql")
    val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
    import spark.implicits._

    //方式一:load方法读取
    spark.read.format("jdbc")
      .option("url", "jdbc:mysql://hadoop102:3306/gmall")
      .option("driver", "com.mysql.jdbc.Driver")
      .option("user", "root")
      .option("password", "root123")
      .option("dbtable", "activity_info")
      .load().show

    //方式二:load方法读取(另一种参数形式)
    spark.read.format("jdbc")
        .options(Map("url"->"jdbc:mysql://hadoop102:3306/gmall?user=root&password=root123",
          "dbtable"->"activity_info","driver"->"com.mysql.jdbc.Driver")).load().show()

    //方式三:使用jdbc读取
    val properties: Properties = new Properties()
    properties.setProperty("user","root")
    properties.setProperty("password","root123")
    spark.read.jdbc("jdbc:mysql://hadoop102:3306/gmall","activity_info",properties).show()

    spark.stop()
  }
}

  3)写入数据(首先在mysql中创建user表)

package com.yuange.spark.day08

import java.util.Properties

import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{Dataset, SaveMode, SparkSession}

case class User3(username: String, age: Int)

/**
 * @作者:袁哥
 * @时间:2021/6/7 22:31
 */
object TestMysqlTwo {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
    //创建SparkSession对象
    val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
    import spark.implicits._

    val rdd: RDD[User3] = spark.sparkContext.makeRDD(List(User3("zhangsan",18),User3("lisi",34)))
    val ds: Dataset[User3] = rdd.toDS()

    //方式一:通用
    ds.write.format("jdbc")
      .option("url", "jdbc:mysql://hadoop102:3306/gmall")
      .option("user", "root")
      .option("password", "root123")
      .option("dbtable", "user")
      .mode(SaveMode.Append)
      .save()

    //方式二:jdbc方式
    val properties: Properties = new Properties()
    properties.setProperty("user","root")
    properties.setProperty("password","root123")
    ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://hadoop102:3306/gmall","user",properties)

    spark.stop()
  }
}

2.8.6 Hive

  Apache Hive Hadoop 上的 SQL 引擎,Spark SQL编译时可以包含 Hive 支持,也可以不包含。包含 Hive 支持的 Spark SQL 可以支持 Hive 表访问、UDF (用户自定义函数)以及 Hive 查询语言(HiveQL/HQL)等。需要强调的一点是,如果要在 Spark SQL 中包含Hive 的库,并不需要事先安装 Hive。一般来说,最好还是在编译Spark SQL时引入Hive支持,这样就可以使用这些特性了。如果你下载的是二进制版本的 Spark,它应该已经在编译时添加了 Hive 支持。

  若要把 Spark SQL 连接到一个部署好的 Hive 上,你必须把 hive-site.xml 复制到 Spark的配置文件目录中($SPARK_HOME/conf)。即使没有部署好 HiveSpark SQL 也可以运行。 需要注意的是,如果你没有部署好HiveSpark SQL 会在当前的工作目录中创建出自己的 Hive 元数据仓库,叫作 metastore_db。此外,如果你尝试使用 HQL 中的 CREATE TABLE (并非 CREATE EXTERNAL TABLE)语句来创建表,这些表会被放在你默认的文件系统中的 /user/hive/warehouse 目录中(如果你的 classpath 中有配好的 hdfs-site.xml,默认的文件系统就是 HDFS,否则就是本地文件系统)

  spark-shell默认是Hive支持的;代码中是默认不支持的,需要手动指定(加一个参数即可)

  1)内嵌的HIVE(在实际使用中, 几乎没有任何人会使用内置的 Hive)

    如果使用 Spark 内嵌的 Hive, 则什么都不用做, 直接使用即可.

    Hive 的元数据存储在 derby , 默认仓库地址:$SPARK_HOME/spark-warehouse

spark.sql("show tables").show

spark.sql("create table testHive(id int)")

spark.sql("show tables").show

    向表加载本地数据

spark.sql("load data local inpath '/opt/module/spark-yarn/bin/data/ids.txt' into table testHive")

spark.sql("select * from testHive").show

  2外部的HIVE

    如果想连接外部已经部署好的Hive,需要通过以下几个步骤

    (1)Spark要接管Hive需要把hive-site.xml拷贝到conf/目录下

cp /opt/module/hive/conf/hive-site.xml /opt/module/spark-yarn/conf/

    (2)Mysql的驱动copyjars/目录下

cp /opt/module/hive/lib/mysql-connector-java-5.1.27-bin.jar /opt/module/spark-yarn/jars/

    (3)如果访问不到hdfs,则需要把core-site.xmlhdfs-site.xml拷贝到conf/目录下

    (4)重启spark-shell

spark.sql("show tables").show

  3运行Spark SQL CLI

    Spark SQL CLI可以很方便的在本地运行Hive元数据服务以及从命令行执行查询任务。在Spark目录下执行如下命令启动Spark SQL CLI,直接执行SQL语句,类似一Hive窗口

bin/spark-sql
show databases;

use default;

show tables;

  4运行Spark beeline

    Spark Thrift ServerSpark社区基于HiveServer2实现的一个Thrift服务。旨在无缝兼容HiveServer2。因为Spark Thrift Server的接口和协议都和HiveServer2完全一致,因此我们部署好Spark Thrift Server后,可以直接使用hivebeeline访问Spark Thrift Server执行相关语句。Spark Thrift Server的目的也只是取代HiveServer2,因此它依旧可以和Hive Metastore进行交互,获取到hive的元数据。

    如果想连接Thrift Server,需要通过以下几个步骤

    (1)Spark要接管Hive需要把hive-site.xml拷贝到conf/目录下

cp /opt/module/hive/conf/hive-site.xml /opt/module/spark-yarn/conf/

    (2)Mysql的驱动copyjars/目录下

cp /opt/module/hive/lib/mysql-connector-java-5.1.27-bin.jar /opt/module/spark-yarn/jars/

    (3)如果访问不到hdfs,则需要把core-site.xmlhdfs-site.xml拷贝到conf/目录下

    (4)启动Thrift Server

sbin/start-thriftserver.sh

    (5)使用beeline连接Thrift Server

bin/beeline -u jdbc:hive2://hadoop102:10000 -n atguigu
show databases;

use default;

show tables;

  5代码操作Hive

    1)导入依赖

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-hive_2.12</artifactId>
    <version>3.0.0</version>
</dependency>
 
<dependency>
    <groupId>org.apache.hive</groupId>
    <artifactId>hive-exec</artifactId>
    <version>3.1.2</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.27</version>
</dependency>

    2hive-site.xml文件拷贝到项目的resources目录中,代码实现

package com.yuange.spark.day08

import org.apache.spark.sql.SparkSession

/**
 * @作者:袁哥
 * @时间:2021/6/7 23:40
 */
object TestMySQLThree {
  def main(args: Array[String]): Unit = {
    //创建SparkSession
    val spark: SparkSession = SparkSession
      .builder()
      .enableHiveSupport()
    .config("spark.sql.warehouse.dir","hdfs:hadoop102:8020/user/hive/warehouse")   //解决hive数据库创建在本地 .master(
"local[*]") .appName("sql") .getOrCreate() } }

    注意:在开发工具中创建数据库默认是在本地仓库,通过参数修改数据库仓库的地址: config("spark.sql.warehouse.dir", "hdfs://hadoop102:8020/user/hive/warehouse")

    如果在执行操作时,出现如下错误:

    可以代码最前面增加如下代码解决(或者以添加系统环境变量的方式也行)(此处的root改为你们自己的hadoop用户名称):

System.setProperty("HADOOP_USER_NAME", "root")

  6)解决数据库创建在本地问题

<!-- 在conf/hive-site.xml文件中添加如下配置 -->
<property> <name>hive.metastore.warehouse.dir</name> <value>hdfs://hadoop102:8020/spark/hiveOnSpark</value> </property>
#或在spark/conf/spark-defaults.conf中添加
spark.sql.warehouse.dir  hdfs://hadoop102:9820/user/hive/warehouse

     完成之后重启(发现HDFS上创建了数据库)

bin/spark-sql
create database testHiveOne;

原文地址:https://www.cnblogs.com/LzMingYueShanPao/p/14857502.html