文章目录
- 第1章 Spark SQL概述
- 1.1 什么是Spark SQL
- 1.2 Spark SQL的特点
- 1.2.1 易整合
- 1.2.2 统一的数据访问方式
- 1.2.3 兼容Hive
- 1.2.4 标准的数据连接
- 1.3 什么是DataFrame
- 1.4 什么是DataSet
- 第2章 Spark SQL编程
- 2.1 SparkSession新的起始点
- 2.2 DataFrame
- 2.2.1 创建DataFrame
- 2.2.2 SQL风格语法
- 2.2.3 DSL风格语法
- 2.2.4 RDD转换为DataFrame
- 2.2.5 DataFrame转换为RDD
- 2.3DataSet
- 2.3.1 创建DataSet
- 2.3.2 RDD转换为DataSet
- 2.3.3 DataSet转换为RDD
- 2.4 DataFrame与DataSet的互操作
- 2.4.1 DataFrame转为DataSet
- 2.4.2 Dataset转为DataFrame
- 2.5 RDD、DataFrame和DataSet之间的关系
- 2.5.1 三者的共性
- 2.5.2 三者的区别
- 2.5.3 三者的互相转化
- 2.6 IDEA创建SparkSQL程序
- 2.7 用户自定义函数
- 2.7.1 UDF
- 2.7.2 UDAF
- 2.7.3 UDTF
- 第3章 SparkSQL数据的加载与保存
- 3.1 通用的加载和保存方式
- 3.1.1 加载数据
- 3.1.2 保存数据
- 3.1.3 默认数据源
- 3.2 JSON文件
- 3.3 MySQL
- 3.3.1 从JDBC读数据
- 3.3.2 向JDBC写数据
- 3.4 Hive
- 3.4.1 使用内嵌Hive
- 3.4.2 外部Hive应用
- 3.4.3 运行Spark SQL CLI
- 3.4.4 代码中操作Hive
第1章 Spark SQL概述
1.1 什么是Spark SQL
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
与基本的Spark RDD API不同,Spark SQL的抽象数据类型为Spark提供了关于数据结构和正在执行的计算的更多信息。
在内部,Spark SQL使用这些额外的信息去做一些额外的优化,有多种方式与Spark SQL进行交互,比如: SQL和DatasetAPI。
当计算结果的时候,使用的是相同的执行引擎,不依赖你正在使用哪种API或者语言。这种统一也就意味着开发者可以很容易在不同的API之间进行切换,这些API提供了最自然的方式来表达给定的转换。
我们已经学习了Hive,它是将Hive SQL转换成 MapReduce然后提交到集群上执行,大大简化了编写 MapReduce的程序的复杂性,由于MapReduce这种计算模型执行效率比较慢。所以Spark SQL的应运而生,它是将Spark SQL转换成RDD,然后提交到集群执行,执行效率非常快!
Spark SQL它提供了2个编程抽象,类似Spark Core中的RDD
1.2 Spark SQL的特点
1.2.1 易整合
无缝的整合了 SQL 查询和 Spark 编程
1.2.2 统一的数据访问方式
使用相同的方式连接不同的数据源
1.2.3 兼容Hive
在已有的仓库上直接运行 SQL 或者 HiveQL
1.2.4 标准的数据连接
通过 JDBC 或者 ODBC 来连接
1.3 什么是DataFrame
在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进行优化。比如下面一个例子:
为了说明查询优化,我们来看上图展示的人口数据分析的示例。图中构造了两个DataFrame,将它们join之后又做了一次filter操作。
如果原封不动地执行这个执行计划,最终的执行效率是不高的。因为join是一个代价较大的操作,也可能会产生一个较大的数据集。如果我们能将filter下推到 join下方,先对DataFrame进行过滤,再join过滤后的较小的结果集,便可以有效缩短执行时间。而Spark SQL的查询优化器正是这样做的。简而言之,逻辑查询计划优化就是一个利用基于关系代数的等价变换,将高成本的操作替换为低成本操作的过程。
1.4 什么是DataSet
DataSet是分布式数据集合。DataSet是Spark 1.6中添加的一个新抽象,是DataFrame的一个扩展。它提供了RDD的优势(强类型,使用强大的lambda函数的能力)以及Spark SQL优化执行引擎的优点。DataSet也可以使用功能性的转换(操作map,flatMap,filter等等)。
第2章 Spark SQL编程
2.1 SparkSession新的起始点
在老的版本中,SparkSQL提供两种SQL查询起始点:一个叫SQLContext,用于Spark自己提供的SQL查询;一个叫HiveContext,用于连接Hive的查询。
SparkSession是Spark最新的SQL查询起始点,实质上是SQLContext和HiveContext的组合,所以在SQLContex和HiveContext上可用的API在SparkSession上同样是可以使用的。SparkSession内部封装了sparkContext,所以计算实际上是由sparkContext完成的。当我们使用 spark-shell 的时候, spark 会自动的创建一个叫做spark的SparkSession, 就像我们以前可以自动获取到一个sc来表示SparkContext
2.2 DataFrame
Spark SQL的DataFrame API 允许我们使用 DataFrame 而不用必须去注册临时表或者生成SQL表达式。DataFrame API 既有transformation操作也有action操作,DataFrame的转换从本质上来说更具有关系, 而 DataSet API 提供了更加函数式的 API
2.2.1 创建DataFrame
在Spark SQL中SparkSession是创建DataFrame和执行SQL的入口,创建DataFrame有三种方式:通过Spark的数据源进行创建;从一个存在的RDD进行转换;还可以从Hive Table进行查询返回。
1)从Spark数据源进行创建
查看Spark支持创建文件的数据源格式
scala> spark.read.
csv jdbc load options parquet table textFile
format json option orc schema text
读取json文件创建DataFrame
scala> spark.read.json("/root/users.json")
res0: org.apache.spark.sql.DataFrame = [Age: bigint, name: string]
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么,如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
2)从RDD进行转换
查看下文
3)Hive Table进行查询返回
查看下文
2.2.2 SQL风格语法
SQL语法风格是指我们查询数据的时候使用SQL语句来查询,这种风格的查询必须要有临时视图或者全局视图来辅助
object SQLDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("sql").setMaster("local[3]")
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
val df: DataFrame = spark.read.json("in/users.json")
df.createTempView("users")
spark.sql("select * from users").show()
}
}
注意:普通临时表是Session范围内的,如果想应用范围内有效,可以使用全局临时表。使用全局临时表时需要全路径访问,如:global_temp.users
df.createGlobalTempView("users")
spark.sql("select * from global_temp.users").show()
2.2.3 DSL风格语法
DataFrame提供一个特定领域语言(domain-specific language, DSL)去管理结构化的数据,可以在 Scala, Java, Python 和 R 中使用 DSL,使用 DSL 语法风格不必去创建临时视图了
object SQLDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("sql").setMaster("local[3]")
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
val df: DataFrame = spark.read.json("in/users.json")
df.printSchema()
//查询name
df.select("name").show()
//查看所有列
df.select("*").show()
//查看name数据以及age+1数据
// 注意:涉及到运算的时候, 每列都必须使用$ 但是idea报错 spark-shell可以通过
df.select(df("name"),df("age")+1).show()
//查看”age”大于”19”的数据
df.filter(df("age")>19).show()
//按照”age”分组,查看数据条数
df.groupBy("age").count().show()
}
}
2.2.4 RDD转换为DataFrame
注意:如果需要RDD与DF或者DS之间操作,那么都需要引入 import spark.implicits._ (spark不是包名,而是sparkSession对象的名称,所以必须先创建SparkSession对象再导入. implicits是一个内部object)
- 前置条件
导入隐式转换并创建一个RDD
在某个目录下准备people.txt
zangsan,12
lisi,22
wangwu,25
val conf: SparkConf = new SparkConf().setAppName("sql").setMaster("local[3]")
val sc = new SparkContext(conf)
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[String] = sc.textFile("in/people.txt")
- 通过手动确定转换
rdd.map(_.split(","))
.map(x=>(x(0),x(1).toInt))
.toDF("name","age")
.show()
- 通过样例类反射转换(常用)
创建一个样例类
case class People(name:String,age:Int)
根据样例类将RDD转换为DataFrame
rdd.map(_.split(","))
.map(x=>People(x(0),x(1).toInt))
.toDF().show()
- 通过编程的方式(了解,一般编程直接操作RDD较少,操作hive或数据文件等较多)
package SQL
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.types.{IntegerType, StringType, StructField, StructType}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.sql.{DataFrame, Row, SparkSession}
object SQLDemo {
case class People(name:String,age:Int)
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("sql").setMaster("local[3]")
val sc = new SparkContext(conf)
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[(String, Int)] = sc.makeRDD(Array(("lisi",20),("wangwu",32),("zs",36)))
// 映射出来一个 RDD[Row], 因为 DataFrame其实就是 DataSet[Row]
val rowRdd: RDD[Row] = rdd.map(x=>Row(x._1,x._2))
//创建StructType类型
val schema = StructType(Array(
StructField("name", StringType),
StructField("age", IntegerType)
))
val df: DataFrame = spark.createDataFrame(rowRdd,schema)
df.show()
}
}
2.2.5 DataFrame转换为RDD
直接调用rdd即可
df.rdd.collect().foreach(println)
注意:得到的RDD存储类型为Row
2.3DataSet
DataSet是具有强类型的数据集合,需要提供对应的类型信息。
2.3.1 创建DataSet
1)使用样例类序列创建DataSet
scala> case class Person(name:String,age:Long)
defined class Person
scala> val ds=Seq(Person("wangwu",21),Person("lisi",20)).toD
toDF toDS
scala> val ds=Seq(Person("wangwu",21),Person("lisi",20)).toDS
ds: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
scala> ds.show()
+------+---+
| name|age|
+------+---+
|wangwu| 21|
| lisi| 20|
+------+---+
2)使用基本类型的序列创建DataSet
scala> val ds=Seq(1,2,3,4,5).toDS
ds: org.apache.spark.sql.Dataset[Int] = [value: int]
scala> ds.show()
+-----+
|value|
+-----+
| 1|
| 2|
| 3|
| 4|
| 5|
+-----+
注意:在实际使用的时候,很少用到把序列转换成DataSet,更多是通过RDD来得到DataSet
2.3.2 RDD转换为DataSet
SparkSQL能够自动将包含有样例类的RDD转换成DataSet,样例类定义了table的结构,样例类属性通过反射变成了表的列名。样例类可以包含诸如Seq或者Array等复杂的结构。
scala> val rdd=sc.textFile("/root/people.txt")
rdd: org.apache.spark.rdd.RDD[String] = /root/people.txt MapPartitionsRDD[1] at textFile at <console>:24
scala> case class Person(name:String,age:Long)
defined class Person
scala> rdd.map(_.split(",")).map(x=>Person(x(0),x(1).toInt)).toDS
res5: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
2.3.3 DataSet转换为RDD
调用rdd方法即可。
scala> res5.rdd
res7: org.apache.spark.rdd.RDD[Person] = MapPartitionsRDD[10] at rdd at <console>:26
2.4 DataFrame与DataSet的互操作
2.4.1 DataFrame转为DataSet
scala> val df=spark.read.json("/root/users.json")
df: org.apache.spark.sql.DataFrame = [Age: bigint, name: string]
scala> case class Person(name:String,age:Long)
defined class Person
scala> df.as[Person]
res10: org.apache.spark.sql.Dataset[Person] = [Age: bigint, name: string]
这种方法就是在给出每一列的类型后,使用as方法,转成Dataset,这在数据类型是DataFrame又需要针对各个字段处理时极为方便。在使用一些特殊的操作时,一定要加上 import spark.implicits._ 不然toDF、toDS无法使用。
2.4.2 Dataset转为DataFrame
scala> case class Person(name:String,age:Long)
defined class Person
scala> val ds=Seq(Person("zs",32),Person("ww",20)).toDS()
ds: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
scala> ds.toDF
res12: org.apache.spark.sql.DataFrame = [name: string, age: bigint]
2.5 RDD、DataFrame和DataSet之间的关系
在SparkSQL中Spark为我们提供了两个新的抽象,分别是DataFrame和DataSet。他们和RDD有什么区别呢?首先从版本的产生上来看:
RDD (Spark1.0) —> Dataframe(Spark1.3) —> Dataset(Spark1.6)
如果同样的数据都给到这三个数据结构,他们分别计算之后,都会给出相同的结果。不同是的他们的执行效率和执行方式。在后期的Spark版本中,DataSet有可能会逐步取代RDD和DataFrame成为唯一的API接口。
2.5.1 三者的共性
- RDD、DataFrame、DataSet全都是spark平台下的分布式弹性数据集,为处理超大型数据提供便利;
- 三者都有惰性机制,在进行创建、转换,如map方法时,不会立即执行,只有在遇到Action如foreach时,三者才会开始遍历运算;
- 三者有许多共同的函数,如filter,排序等;
- 在对DataFrame和Dataset进行操作许多操作都需要这个包:import spark.implicits._(在创建好SparkSession对象后尽量直接导入)
- 三者都会根据 Spark 的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出
- 三者都有partition的概念
- DataFrame和Dataset均可使用模式匹配获取各个字段的值和类型
2.5.2 三者的区别
- RDD
- RDD一般和Spark MLib同时使用
- RDD不支SparkSQL操作
- DataFrame
- 与RDD和Dataset不同,DataFrame每一行的类型固定为Row,每一列的值没法直接访问,只有通过解析才能获取各个字段的值
- DataFrame与DataSet一般不与 Spark MLib 同时使用
- DataFrame与DataSet均支持 SparkSQL 的操作,比如select,groupby之类,还能注册临时表/视窗,进行 sql 语句操作
- DataFrame与DataSet支持一些特别方便的保存方式,比如保存成csv,可以带上表头,这样每一列的字段名一目了然
- DataSet
- Dataset和DataFrame拥有完全相同的成员函数,区别只是每一行的数据类型不同。 DataFrame其实就是DataSet的一个特例
type DataFrame = Dataset[Row]
- DataFrame也可以叫Dataset[Row],每一行的类型是Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用上面提到的getAS方法或者共性中的第七条提到的模式匹配拿出特定字段。而Dataset中,每一行是什么类型是不一定的,在自定义了case class之后可以很自由的获得每一行的信息
2.5.3 三者的互相转化
2.6 IDEA创建SparkSQL程序
- 添加依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>2.1.1</version>
</dependency>
- 代码实现
object SparkSQL_Demo {
def main(args: Array[String]): Unit = {
//创建上下文环境配置对象
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL01_Demo")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
//RDD=>DataFrame=>DataSet转换需要引入隐式转换规则,否则无法转换
//spark不是包名,是上下文环境对象名
import spark.implicits._
//读取json文件 创建DataFrame {"username": "lisi","age": 18}
val df: DataFrame = spark.read.json("D:\\dev\\workspace\\spark-bak\\spark-bak-00\\input\\test.json")
//df.show()
//SQL风格语法
df.createOrReplaceTempView("user")
//spark.sql("select avg(age) from user").show
//DSL风格语法
//df.select("username","age").show()
//*****RDD=>DataFrame=>DataSet*****
//RDD
val rdd1: RDD[(Int, String, Int)] = spark.sparkContext.makeRDD(List((1,"qiaofeng",30),(2,"xuzhu",28),(3,"duanyu",20)))
//DataFrame
val df1: DataFrame = rdd1.toDF("id","name","age")
//df1.show()
//DateSet
val ds1: Dataset[User] = df1.as[User]
//ds1.show()
//*****DataSet=>DataFrame=>RDD*****
//DataFrame
val df2: DataFrame = ds1.toDF()
//RDD 返回的RDD类型为Row,里面提供的getXXX方法可以获取字段值,类似jdbc处理结果集,但是索引从0开始
val rdd2: RDD[Row] = df2.rdd
//rdd2.foreach(a=>println(a.getString(1)))
//*****RDD=>DataSe*****
rdd1.map{
case (id,name,age)=>User(id,name,age)
}.toDS()
//*****DataSet=>=>RDD*****
ds1.rdd
//释放资源
spark.stop()
}
}
case class User(id:Int,name:String,age:Int)
2.7 用户自定义函数
2.7.1 UDF
输入一行,返回一个结果。在Shell窗口中可以通过spark.udf功能用户可以自定义函数。
scala> val df=spark.read.json("/root/users.json")
df: org.apache.spark.sql.DataFrame = [Age: bigint, name: string]
//注册UDF,功能为在数据前添加字符串
scala> spark.udf.register("addName",(x:String)=>"Name:"+x)
res0: org.apache.spark.sql.expressions.UserDefinedFunction = UserDefinedFunction(<function1>,StringType,Some(List(StringType)))
scala> df.createOrReplaceTempView("people")
//应用UDF
scala> spark.sql("select addName(name),age from people").show()
+-----------------+----+
|UDF:addName(name)| age|
+-----------------+----+
| Name:Michael|null|
| Name:Andy| 30|
| Name:Justin| 19|
+-----------------+----+
2.7.2 UDAF
输入多行,返回一行。强类型的Dataset和弱类型的DataFrame都提供了相关的聚合函数, 如?count(),countDistinct(),avg(),max(),min()。除此之外,用户可以设定自己的自定义聚合函数。通过继承UserDefinedAggregateFunction来实现用户自定义聚合函数。
需求:实现求平均年龄
- RDD算子方式实现
object SQLDemo {
case class People(name:String,age:Int)
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("sql").setMaster("local[3]")
val sc = new SparkContext(conf)
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("zhangsan",20),("lisi",30),("wangwu",36)))
//转换结构
val mapRdd: RDD[(Int, Int)] = rdd.map {
case (name, age) => {
(age, 1)
}
}
//对年龄以及总人数进行聚合操作
val res: (Int, Int) = mapRdd.reduce {
(t1, t2) => {
(t1._1 + t2._1, t1._2 + t2._2)
}
}
println(res._1 / res._2)
}
}
- 自定义累加器方式实现(减少Shuffle)提高效率(模仿LongAccumulator累加器)
object SQLDemo {
case class People(name:String,age:Int)
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("sql").setMaster("local[3]")
val sc = new SparkContext(conf)
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("zhangsan",20),("lisi",30),("wangwu",36)))
//创建累加器对象
val accumulator = new MyAccumulator
//注册累加器
sc.register(accumulator)
//使用累加器
rdd.foreach{
case(name,age)=>{
accumulator.add(age)
}
}
//获取累加器的值
println(accumulator.value)
}
}
class MyAccumulator extends AccumulatorV2[Int,Double]{
var ageSum:Int=0
var countSum:Int=0
override def isZero: Boolean = {
ageSum==0&&countSum==0
}
override def copy(): AccumulatorV2[Int, Double] = {
val accumulator = new MyAccumulator
accumulator.ageSum=this.ageSum
accumulator.countSum=this.countSum
accumulator
}
override def reset(): Unit = {
ageSum=0
countSum=0
}
override def add(v: Int): Unit = {
ageSum+=v
countSum+=1
}
override def merge(other: AccumulatorV2[Int, Double]): Unit = {
other match {
case ma:MyAccumulator =>{
this.ageSum+=ma.ageSum
this.countSum+=ma.countSum
}
case _=>
}
}
override def value: Double = {
ageSum.toDouble/countSum
}
}
- 自定义聚合函数实现-弱类型(应用于SparkSQL更方便)
object SQLDemo {
case class People(name:String,age:Int)
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("sql").setMaster("local[3]")
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
val sc: SparkContext = spark.sparkContext
//读取json文件
val df: DataFrame = spark.read.json("in/users.json")
//创建临时视图
df.createOrReplaceTempView("user")
//创建自定义函数对象
val myAvg = new MyAvg
//注册自定义函数
spark.udf.register("myavg",myAvg)
//使用聚合函数查询
spark.sql("select myavg(age) from user").show()
}
}
//自定义UDAF函数(若类型)
class MyAvg extends UserDefinedAggregateFunction{
//聚合函数的输入数据类型
override def inputSchema: StructType = {
StructType(Array(StructField(
"age",IntegerType
)))
}
//缓存数据的类型
override def bufferSchema: StructType = {
StructType(Array(
StructField("sum",IntegerType),
StructField("count",IntegerType)
))
}
//聚合函数返回的数据类型
override def dataType: DataType = DoubleType
//稳定性 默认不处理 直接返回true 相同的输入是否会得到相同的输出
override def deterministic: Boolean = true
//初始化 缓存设置到初始状态
override def initialize(buffer: MutableAggregationBuffer): Unit = {
//让缓存中年龄总和归0
buffer(0)=0
//让缓存中总人数归0
buffer(1)=0
}
//更新缓存数据
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
if (!buffer.isNullAt(0)){//buffer.getAs[Int](0)
buffer(0)=buffer.getInt(0)+input.getInt(0)
buffer(1)=buffer.getInt(1)+1}
}
//分区间合并
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1(0)=buffer1.getInt(0)+buffer2.getInt(0)
buffer1(1)=buffer1.getInt(1)+buffer2.getInt(1)
}
//计算逻辑
override def evaluate(buffer: Row): Any = {
buffer.getInt(0).toDouble/buffer.getInt(1)
}
}
4)自定义聚合函数实现-强类型(应用于DataSet的DSL更方便)
object SQLDemo {
case class People(name:String,age:Int)
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("sql").setMaster("local[3]")
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
val sc: SparkContext = spark.sparkContext
import spark.implicits._
//读取json文件
val df: DataFrame = spark.read.json("in/users.json")
//创建临时视图
// df.createOrReplaceTempView("user")
//创建自定义函数对象
val myAvg = new MyAvg
//注意:如果是自定义UDAF的强类型,没有办法应用SQl风格
//注册自定义函数
//spark.udf.register("myavg",myAvg)
//使用聚合函数查询
//spark.sql("select myavg(age) from user").show()
val ds: Dataset[User] = df.as[User]
//将自定义函数对象转换为查询列
val col: TypedColumn[User, Double] = myAvg.toColumn
//在进行查询的时候,会将查询出来的记录交给自定义函数进行处理
ds.select(col).show()
}
}
//输入类型的样例类
case class User(name:String,age:Long)
//缓存类型
case class AgeBuffer(var sum:Long,var count:Long)
/**
* 定义类继承org.apache.spark.sql.expressions.Aggregator
* 重写类中的方法
*/
//自定义UDAF函数(强类型)
class MyAvg extends Aggregator[User,AgeBuffer,Double]{
//对缓存数据进行初始化
override def zero: AgeBuffer = {
AgeBuffer(0L,0L)
}
//对当前分区内的数据进行聚合
override def reduce(b: AgeBuffer, a: User): AgeBuffer = {
b.sum+=a.age
b.count+=1
b
}
//分区间合并
override def merge(b1: AgeBuffer, b2: AgeBuffer): AgeBuffer = {
b1.sum+=b2.sum
b1.count+=b2.count
b1
}
//返回计算结果
override def finish(reduction: AgeBuffer): Double = {
reduction.sum.toDouble/reduction.count
}
//DataSet编码以及解码器,用于序列化,固定写法
//用户自定义Ref类型 product 系统值类型,根据具体类型进行选择
override def bufferEncoder: Encoder[AgeBuffer] = {
Encoders.product
}
override def outputEncoder: Encoder[Double] = {
Encoders.scalaDouble
}
}
2.7.3 UDTF
输入一行,返回多行(hive);
SparkSQL中没有UDTF,spark中用flatMap即可实现该功能,这里演示调用hive创建UDTF
package SQL
import java.util
import org.apache.hadoop.hive.ql.exec.UDFArgumentException
import org.apache.hadoop.hive.ql.udf.generic.GenericUDTF
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory
import org.apache.hadoop.hive.serde2.objectinspector.{ObjectInspector, ObjectInspectorFactory, PrimitiveObjectInspector, StructObjectInspector}
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, SparkSession}
class MyUDTF extends GenericUDTF{
override def initialize(argOIs: Array[ObjectInspector]): StructObjectInspector = {
if (argOIs.length!=1){
throw new UDFArgumentException("有且只能有一个参数传入")
}
if (argOIs(0).getCategory!=ObjectInspector.Category.PRIMITIVE){
throw new UDFArgumentException("参数类型不匹配")
}
val fieldNames=new util.ArrayList[String]
val fieldOIs=new util.ArrayList[ObjectInspector]
fieldNames.add("type")
//这里定义的输出字段的类型
fieldOIs.add(PrimitiveObjectInspectorFactory
.javaStringObjectInspector)
ObjectInspectorFactory.
getStandardStructObjectInspector(fieldNames,fieldOIs)
}
//传入 hadoop scala kafa
//输出 hadoop
// scala
// kafa
override def process(objects: Array[AnyRef]): Unit = {
//将字符串切分成的单个单词
val strings: Array[String] = objects(0).toString.split(" ")
println(strings)
for (str <- strings) {
val tmp = new Array[String](1)
tmp(0)=str
forward(tmp)
}
}
override def close(): Unit = {}
}
object SparkUDTFDemo {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder().appName("function").master("local[3]").enableHiveSupport().getOrCreate()
val sc: SparkContext = spark.sparkContext
import spark.implicits._
val lines: RDD[String] = sc.textFile("in/word.txt")
val stuDF: DataFrame = lines.map(_.split("//"))
.filter(x => x(1).equals("ls"))
.map(x => (x(0), x(1), x(2)))
.toDF("id", "name", "class")
stuDF.createOrReplaceTempView("student")
spark.sql("CREATE TEMPORARY FUNCTION MyUDTF AS 'SQL.MyUDTF'")
spark.sql("select MyUDTF(class) from student").show()
}
}
注意:
需要使用enableHiveSupport()
as后面的方法名称要全路径
第3章 SparkSQL数据的加载与保存
3.1 通用的加载和保存方式
spark
.read.load 是加载数据的通用方法
df
.write.save 是保存数据的通用方法
3.1.1 加载数据
- read直接加载数据
scala> spark.read.
csv jdbc load options parquet table textFile
format json option orc schema text
注意:加载数据的相关参数需写到上述方法中,如:textFile需传入加载数据的路径,jdbc需传入JDBC相关参数。
scala> spark.read.json("/root/users.json").show()
+----+-------+
| Age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
- format指定加载数据类型
scala> spark.read.format("…")[.option("…")].load("…")
用法详解:
- format("…"):指定加载的数据类型,包括"csv"、“jdbc”、“json”、“orc”、“parquet"和"textFile”
- load("…"):在"csv"、“jdbc”、“json”、“orc”、"parquet"和"textFile"格式下需要传入加载数据的路径
- option("…"):在"jdbc"格式下需要传入JDBC相应参数,url、user、password和dbtable
scala> spark.read.format("json").load("/root/users.json").show()
+----+-------+
| Age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
- 在文件上直接运行SQL
我们前面都是使用read API 先把文件加载到 DataFrame然后再查询,其实,我们也可以直接在文件上进行查询
scala> spark.sql("select * from json.`/root/users.json`").show()
+----+-------+
| Age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
注意: json表示文件的格式. 后面的文件具体路径需要用反引号括起来.
3.1.2 保存数据
- write直接保存数据
scala> df.write.
bucketBy format jdbc mode options parquet save sortBy
csv insertInto json option orc partitionBy saveAsTable text
注意:保存数据的相关参数需写到上述方法中。如:textFile需传入加载数据的路径,jdbc需传入JDBC相关参数。
例如:直接将df中数据保存到指定目录
//默认保存格式为parquet
scala> df.write.save("/opt/module/spark-local/output")
//可以指定为保存格式,直接保存,不需要再调用save了
scala> df.write.json("/opt/module/spark-local/output")
- format指定保存数据类型
scala> df.write.format("…")[.option("…")].save("…")
用法详解:
- format("…"):指定保存的数据类型,包括"csv"、“jdbc”、“json”、“orc”、“parquet"和"textFile”。
- save ("…"):在"csv"、“orc”、"parquet"和"textFile"格式下需要传入保存数据的路径。
- option("…"):在"jdbc"格式下需要传入JDBC相应参数,url、user、password和dbtable
- 文件保存选项
保存操作可以使用 SaveMode, 用来指明如何处理数据,使用mode()方法来设置。
有一点很重要: 这些 SaveMode 都是没有加锁的, 也不是原子操作。
SaveMode是一个枚举类,其中的常量包括:
Scala/Java | Any Language | Meaning |
SaveMode.ErrorIfExists(default) | "error"(default) | 如果文件已经存在则抛出异常 |
SaveMode.Append | "append" | 如果文件已经存在则追加 |
SaveMode.Overwrite | "overwrite" | 如果文件已经存在则覆盖 |
SaveMode.Ignore | "ignore" | 如果文件已经存在则忽略 |
例如:使用指定format指定保存类型进行保存
df.write.mode("append").json("/opt/module/spark-local/output")
3.1.3 默认数据源
Spark SQL的默认数据源为Parquet格式。数据源为Parquet文件时,Spark SQL可以方便的执行所有的操作,不需要使用format。修改配置项spark.sql.sources.default,可修改默认数据源格式。
- 加载数据
scala> spark.read.load("/root/out").show()
+----+-------+
| Age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
- 保存数据
scala> var df = spark.read.json("/root/users.json")
//保存为parquet格式
scala> df.write.mode("append").save("/root/out")
3.2 JSON文件
Spark SQL 能够自动推测 JSON数据集的结构,并将它加载为一个Dataset[Row]. 可以通过SparkSession.read.json()去加载一个?一个JSON 文件。
注意:这个JSON文件不是一个传统的JSON文件,每一行都得是一个JSON串。格式如下:
{"name":"Michael"}
{"name":"Andy","age":30}
{"name":"Justin","age":19}
- 导入隐式转换
import spark.implicits._
- 加载JSON文件
val path = "/root/users.json"
val peopleDF = spark.read.json(path)
- 创建临时表
peopleDF.createOrReplaceTempView("people")
- 数据查询
val teenagerNamesDF = spark.sql("SELECT name FROM people WHERE age BETWEEN 13 AND 19").show()
3.3 MySQL
Spark SQL可以通过JDBC从关系型数据库中读取数据的方式创建DataFrame,通过对DataFrame一系列的计算后,还可以将数据再写回关系型数据库中。
如果使用spark-shell操作,可在启动shell时指定相关的数据库驱动路径或者将相关的数据库驱动放到spark的类路径下。
bin/spark-shell
--jars mysql-connector-java-5.1.27-bin.jar
我们这里只演示在Idea中通过JDBC对Mysql进行操作
导入依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
3.3.1 从JDBC读数据
object SQLDemo {
case class People(name:String,age:Int)
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("sql").setMaster("local[3]")
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
val sc: SparkContext = spark.sparkContext
import spark.implicits._
val url="jdbc:mysql://hadoop100:3306/test"
val driver="com.mysql.jdbc.Driver"
val user="root"
val password="ok"
//方式1:通用load方法读取
spark.read.format("jdbc")
.option("url",url)
.option("driver",driver)
.option("user",user)
.option("password",password)
.option("dbtable","student")
.load().show()
//方式2:通用的load方法读取,参数的另一种形式
spark.read.format("jdbc")
.options(Map(
"url"->url,
"driver"->driver,
"user"->user,
"password"->password
))
//方式3:使用jdbc方法读取
val props = new Properties()
props.setProperty("user",user)
props.setProperty("password",password)
spark.read.jdbc(url,"student",props).show()
}
}
3.3.2 向JDBC写数据
object SQLDemo {
//这里的字段名称需要与sql数据名称相同
case class People(sname:String,sage:Int)
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("sql").setMaster("local[3]")
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
val sc: SparkContext = spark.sparkContext
import spark.implicits._
val ds: Dataset[People] = sc.makeRDD(List(People("zs",20),People("ls",25))).toDS
val url="jdbc:mysql://hadoop100:3306/test"
val driver="com.mysql.jdbc.Driver"
val user="root"
val password="ok"
//方式1:通用的方式 format指定写出类型
ds.write.format("jdbc")
.option("url",url)
.option("driver",driver)
.option("user",user)
.option("password",password)
.option("dbtable","student")
.mode(SaveMode.Append)
.save()
//方式2:通过jdbc方法
val props = new Properties()
props.setProperty("user",user)
props.setProperty("password",password)
ds.write.mode(SaveMode.Append).jdbc(url,"student",props)
spark.stop()
}
}
3.4 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)。即使没有部署好 Hive,Spark SQL 也可以运行,需要注意的是,如果你没有部署好Hive,Spark SQL 会在当前的工作目录中创建出自己的 Hive 元数据仓库,叫作 metastore_db。此外,对于使用部署好的Hive,如果你尝试使用 HiveQL 中的 CREATE TABLE (并非 CREATE EXTERNAL TABLE)语句来创建表,这些表会被放在你默认的文件系统中的 /user/hive/warehouse 目录中(如果你的 classpath 中有配好的 hdfs-site.xml,默认的文件系统就是 HDFS,否则就是本地文件系统)。
spark-shell默认是Hive支持的;代码中是默认不支持的,需要手动指定(加一个参数即可)。
3.4.1 使用内嵌Hive
如果使用 Spark 内嵌的 Hive, 则什么都不用做, 直接使用即可.
Hive 的元数据存储在 derby 中, 仓库地址:$SPARK_HOME/spark-warehouse
scala> spark.sql("show tables").show
+--------+---------+-----------+
|database|tableName|isTemporary|
+--------+---------+-----------+
+--------+---------+-----------+
scala> spark.sql("create table aa(id int)")
19/02/09 18:36:10 WARN HiveMetaStore: Location: file:/opt/module/spark-local/spark-warehouse/aa specified for non-external table:aa
res2: org.apache.spark.sql.DataFrame = []
scala> spark.sql("show tables").show
+--------+---------+-----------+
|database|tableName|isTemporary|
+--------+---------+-----------+
| default| aa| false|
+--------+---------+-----------+
向表中加载本地数据数据
scala> spark.sql("load data local inpath './ids.txt' into table aa")
res8: org.apache.spark.sql.DataFrame = []
scala> spark.sql("select * from aa").show
+---+
| id|
+---+
|100|
|101|
|102|
|103|
|104|
|105|
|106|
+---+
然而在实际使用中, 几乎没有任何人会使用内置的 Hive
3.4.2 外部Hive应用
如果Spark要接管Hive外部已经部署好的Hive,需要通过以下几个步骤。
- 确定原有Hive是正常工作的
- 需要把hive-site.xml拷贝到spark的conf/目录下
- 如果以前hive-site.xml文件中,配置过Tez相关信息,注释掉
- 把Mysql的驱动copy到Spark的jars/目录下
- 需要提前启动hive服务,hive/bin/hiveservices.sh start
- 如果访问不到hdfs,则需把core-site.xml和hdfs-site.xml拷贝到conf/目录
scala> spark.sql("show tables").show
+--------+------------------+-----------+
|database| tableName|isTemporary|
+--------+------------------+-----------+
| default| emp_id| false|
| default| employee| false|
| default| employee_id| false|
| default| employee_p| false|
| default|employee_partition| false|
| default| p_test| false|
| default| tmp_employee| false|
| default| toronto| false|
| default| userinfos| false|
+--------+------------------+-----------+
scala> spark.sql("select * from employee").show
+-------+-------------------+------------+--------------+--------------------+
| name| address| info| technol| jobs|
+-------+-------------------+------------+--------------+--------------------+
|Michael|[Montreal, Toronto]| [Male, 30]| [DB -> 80]|[Product -> Devel...|
| Will| [Montreal]| [Male, 35]| [Perl -> 85]|[Product -> Lead,...|
|Shelley| [New York]|[Female, 27]|[Python -> 80]|[Test -> Lead, CO...|
| Lucy| [Vancouver]|[Female, 57]| [Sales -> 89]| [Sales -> Lead]|
+-------+-------------------+------------+--------------+--------------------+
3.4.3 运行Spark SQL CLI
Spark SQLCLI可以很方便的在本地运行Hive元数据服务以及从命令行执行查询任务。在Spark目录下执行如下命令启动Spark SQ LCLI,直接执行SQL语句,类似Hive窗口。
3.4.4 代码中操作Hive
- 添加依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-hive_2.11</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-exec</artifactId>
<version>1.2.1</version>
</dependency>
- 拷贝hive-site.xml到resources目录
- 代码实现
object SQLDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("sql").setMaster("local[3]")
val spark: SparkSession = SparkSession
.builder()
.config(conf)
.enableHiveSupport()
.getOrCreate()
spark.sql("show tables").show()
}
}