spark 系列
Spark SQL 详解
前言
看了前面的几篇 Spark 博客,相信大家对于 Spark 的基础框架、RDD和核心 SparkCore 已经搞明白了。本篇博客将为大家详细介绍了 Spark 一个重要的内置模块 Spark SQL 。这是工作中最常用的一个内置模块之一。
Spark SQL 简介
什么是Spark SQL?
Spark SQL是Spark用来处理结构化数据的一个模块,它提供了2个编程抽象:DataFrame和DataSet,并且作为分布式SQL查询引擎的作用。
我们已经学习了Hive,它是将Hive SQL转换成MapReduce然后提交到集群上执行,大大简化了编写MapReduce的程序的复杂性,由于MapReduce这种计算模型执行效率比较慢。所以Spark SQL的应运而生,它是将Spark SQL转换成RDD,然后提交到集群执行,执行效率非常快!同时Spark SQL也支持从Hive中读取数据。
Spark SQL 的由来
SQL是一种传统的用来进行数据分析的标准,为了解决SQL on hadoop的问题,早期有以下几种方案:
- Hive是原始的SQL-on-Hadoop解决方案
- Impala:和Hive一样,提供了一种可以针对已有Hadoop数据编写SQL查询的方法
- Presto:类似于Impala,未被主要供应商支持
- Shark:Spark SQL的前身,设计目标是作为Hive的一个补充
- Phoenix:基于HBase的开源SQL查询引擎
Shark的初衷:让Hive运行在Spark之上。Spark 是对Hive的改造,继承了大量Hive代码,给优化和维护带来了大量的麻烦。
Spark SQL 的特点
1. 易整合(集成)
2. 统一的数据访问方式
3. 兼容Hive
4. 标准的数据连接
Spark SQL 框架结构
如上图所示,Spark SQL有以下主要功能:
- Spark SQL是Spark的核心组件之一(2014.4 Spark1.0)
- 能够直接访问现存的Hive数据
- 提供JDBC/ODBC接口供第三方工具借助Spark进行数据处理
- 提供了更高层级的接口方便地处理数据
- 支持多种操作方式:SQL、API编程
- 支持多种外部数据源:Parquet、JSON、RDBMS等
Spark SQL的核心 Catalyst优化器(了解)
Catalyst优化器是Spark SQL的核心。
在 深入研究Spark SQL的Catalyst优化器(原创翻译) 对Spark SQL的核心 Catalyst优化器的原理写的很清楚,这里我就简单的说一下 Catalyst优化器 优化一条SQL语句的大致流程。
例如,如下一条简单的SQL语句,Catalyst优化器是如何去优化的呢?
SELECT name FROM
(
SELECT id, name FROM people
) p
WHERE p.id = 1
如下图所示,Catalyst优化器的优化主要在投影上面查询过滤器并检查过滤是否可下压。一句话概括来说,Catalyst优化器的优化会自动选择最优执行效率的方案,这在过滤删选操作是非常实用的,可以减少hive SQL所必要的子查询嵌套,使代码变得简单一点。
Spark SQL 常用API
SparkContext 与 SparkSession
在Spark 2.0
以前的版本中,SparkSQL提供两种SQL查询起始点:一个叫SQLContext,用于Spark自己提供的SQL查询编程入口;一个叫HiveContext,是SQLContext的子集,提供更多的功能,主要用于连接Hive的查询。
SparkSession是Spark最新的SQL查询起始点,实质上是SQLContext和HiveContext的组合,所以在SQLContext和HiveContext上可用的API在SparkSession上同样是可以使用的。SparkSession内部封装了sparkContext,所以计算实际上是由sparkContext完成的。SparkSession提供与Spark功能交互单一入口点,并允许使用DataFrame和Dataset API对Spark进行编程。
SparkSession 创建语法:
val spark = SparkSession.builder.master("master").appName("appName").getOrCreate()
注意:
- 无特殊说明时,下文中“spark”均指SparkSession实例;
- 如果是spark-shell下,会自动创建“sc”和“spark”;
- master通常为
local[*]
,制定开启CPU核数进行计算,*
可为任何整数,如果为*
本身,代表动用计算机现有可用CPU核数。
在spark的早期版本中,SparkContext是spark的主要切入点,由于RDD是主要的API,我们通过sparkcontext来创建和操作RDD。对于每个其他的API,我们需要使用不同的context。例如,对于Streming,我们需要使用StreamingContext;对于sql,使用sqlContext;对于Hive,使用hiveContext。但是随着DataSet和DataFrame的API逐渐成为标准的API,就需要为他们建立接入点。
SparkContext 创建语法:
val conf = new SparkConf().setMaster("master").setAppName("appName")
val sc = new SparkContext(conf)
创建sparkContext对象:主要用于读取需要处理的数据,封装在RDD集合中;调度jobs执行。
SparkSession内部封装了SparkContext,所以计算实际上是由SparkContext完成的。
DataFrame
与RDD类似,DataFrame也是一个分布式数据容器。然而DataFrame更像传统数据库的二维表格,除了数据以外,还记录数据的结构信息,即schema。同时,与Hive类似,DataFrame也支持嵌套数据类型(struct、array和map)。从API易用性的角度上看,DataFrame API提供的是一套高层的关系操作,比函数式的RDD API要更加友好,门槛更低。
上图直观地体现了DataFrame和RDD的区别。左侧的RDD[Person]虽然以Person为类型参数,但Spark框架本身不了解Person类的内部结构。而右侧的DataFrame却提供了详细的结构信息,使得Spark SQL可以清楚地知道该数据集中包含哪些列,每列的名称和类型各是什么。DataFrame是为数据提供了Schema的视图。可以把它当做数据库中的一张表来对待,DataFrame也是懒执行的。性能上比RDD要高。
RDD是分布式的 Java对象的集合。DataFrame是分布式的Row对象的集合。DataFrame除了提供了比RDD更丰富的算子以外,更重要的特点是提升执行效率、减少数据读取以及执行计划的优化。
创建方式与使用
在Spark SQL中SparkSession是创建DataFrame和执行SQL的入口,创建DataFrame有三种方式:通过Spark的数据源进行创建;从一个存在的RDD进行转换;还可以从Hive Table进行查询返回。
1. 通过Spark的数据源进行创建
json.txt 源数据:
{"name":"hubert","age":"100"}
{"name":"rose","age":"10"}
{"name":"jack","age":"39"}
{"name":"henry","age":"34"}
val spark = SparkSession.builder().appName("test").master("local[*]").getOrCreate()
// 读取json文件
val df = spark.read.format("json").load("data/json.txt")
// 操作
df.select("name","age").show(false)
spark.close()
// 结果
+------+---+
|name |age|
+------+---+
|hubert|100|
|rose |10 |
|jack |39 |
|henry |34 |
+------+---+
2. 从RDD中转换
下面 RDD转换为DateFrame 会细说。
3. 从Hive Table进行查询返回
这个将在后面的博文中涉及到,这里暂且不谈。
SQL风格语法(主要)
object Test04 {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().appName("test").master("local[*]").getOrCreate()
// 1)创建一个DataFrame
val df = spark.read.json("data/json.txt")
// 2)对DataFrame创建一个临时表
df.createOrReplaceTempView("people")
// 3)通过SQL语句实现查询全表
val sqlDF = spark.sql("SELECT * FROM people")
// 4)结果展示
sqlDF.show(false)
// 5)对于DataFrame创建一个全局表
df.createGlobalTempView("people")
// 6)通过SQL语句实现查询全表
spark.sql("select * from global_temp.people").show(false)
spark.close()
}
}
注意:临时表是Session范围的,Session退出后,表就失效了。如果想应用范围内仍有效,可以使用全局表。注意使用全局表时需要全路径访问,如:global_temp:people。
全局的临时视图存在于系统数据库 global_temp中,我们必须加上库名去引用它
DSL 风格语法 (次要)
object Test04 {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().appName("test").master("local[*]").getOrCreate()
// 导入 sparkSession 对象spark的隐式类
import spark.implicits._
// 1)创建一个DataFrame
val df = spark.read.json("data/json.txt")
// 2)查看DataFrame的Schema信息
df.printSchema()
// 3)只查看"name"列数据
df.select("name").show(false)
// 4)查看"name"列数据以及"age+1"数据
df.select($"name",$"age" + 1).show(false)
// 5)查看"age"大于"21"的数据
df.filter($"age" >21).show(false)
// 6)按照"age"分组,查看数据条数
df.groupBy("age").count().show()
spark.close()
}
}
结果:
root
|-- age: string (nullable = true)
|-- name: string (nullable = true)
+------+
|name |
+------+
|hubert|
|rose |
|jack |
|henry |
+------+
+------+---------+
|name |(age + 1)|
+------+---------+
|hubert|101.0 |
|rose |11.0 |
|jack |40.0 |
|henry |35.0 |
+------+---------+
+---+------+
|age|name |
+---+------+
|100|hubert|
|39 |jack |
|34 |henry |
+---+------+
+---+-----+
|age|count|
+---+-----+
| 34| 1|
|100| 1|
| 10| 1|
| 39| 1|
+---+-----+
RDD转换为DateFrame
注意:如果需要RDD与DF或者DS之间操作,那么都需要引入import spark.implicits._
【spark不是包名,而是sparkSession对象的名称】
1. 元组方式导入
object MyDf {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local[*]").appName("test").getOrCreate()
import spark.implicits._
val df= spark.sparkContext.textFile("data/ds.txt").map(line=>{
val strs = line.split(" ")
(strs(0).toInt,strs(1),strs(2).toInt)
}).toDF("userID","userName","age")
df.show(false)
}
}
元组方式需要类型转换。
2. 样例类方式导入
// 定义样例类
case class UserInfo1(userId:Int,userName:String,age:Int)
object MyDf {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local[*]").appName("test").getOrCreate()
import spark.implicits._
val df= spark.sparkContext.textFile("data/ds.txt").map(line=>{
val strs = line.split(" ")
UserInfo1(strs(0).toInt,strs(1),strs(2).toInt)
}).toDF
df.show(false)
}
}
样例类无需进行类型转换。
3. 指定Row-Schema编程导入
object MyDf {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local[*]").appName("test").getOrCreate()
val rdd = spark.sparkContext.textFile("file:///F:\\IT\\study\\java\\myspark01\\data\\ds.txt")
.map(line=>{
val strs = line.split(" ")
Row(strs(0).toInt,strs(1),strs(2).toInt)
})
// 创建 结构schema
val schema = StructType(Seq(
StructField("useId",IntegerType,false),
StructField("userName",StringType,false),
StructField("age",IntegerType,false)
))
val df = spark.createDataFrame(rdd,schema)
df.show(false)
}
}
这种方式不常用,比较复杂。
DateFrame 转换为RDD
直接调用rdd即可。
object MyDf {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local[*]").appName("test").getOrCreate()
import spark.implicits._
val rdd = spark.sparkContext.textFile("data/ds.txt").rdd
}
}
DataSet
1)是Dataframe API(Spark 1.6之后)
的一个扩展,是Spark最新的数据抽象。
2)用户友好的API风格,既具有类型安全检查也具有Dataframe的查询优化特性。
3)Dataset支持编解码器,当需要访问非堆上的数据时可以避免反序列化整个对象,提高了效率。
4)样例类被用来在Dataset中定义数据的结构信息,样例类中每个属性的名称直接映射到DataSet中的字段名称。
5)Dataframe是Dataset的特列,DataFrame=Dataset[Row] ,所以可以通过as方法将Dataframe转换为Dataset。Row是一个类型,跟Car、Person这些的类型一样,所有的表结构信息我都用Row来表示。
6)DataSet是强类型的。可以使用强大的lambda函数的能力,比如可以有Dataset[Car],Dataset[Person]。
7)DataFrame只是知道字段,但是不知道字段的类型,所以在执行这些操作的时候是没办法在编译的时候检查是否类型失败的,比如你可以对一个String进行减法操作,在执行的时候才报错,而DataSet不仅仅知道字段,而且知道字段类型,所以有更严格的错误检查。就跟JSON对象和类对象之间的类比。
8)一个Dataset 可以从JVM对象构造,然后使用函数转换(map, flatMap,filter等)去操作。 Dataset API 支持Scala和Java。 Python不支持Dataset API。
创建方式
1)创建一个样例类
scala> case class Person(name: String, age: Long)
defined class Person
2)创建DataSet
scala> val caseClassDS = Seq(Person("Andy", 32)).toDS()
caseClassDS: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
RDD转换为DataSet
SparkSQL能够自动将包含有case类的RDD转换成DataFrame,case类定义了table的结构,case类属性通过反射变成了表的列名。
case class UserInfo(userId:Int,userName:String,birthday:String)
object MyDs {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local[*]").appName("test").getOrCreate()
val rdd = spark.sparkContext.textFile("data/ds.txt").map(line=>{
val strs = line.split(" ")
UserInfo1(strs(0).toInt,strs(1),strs(2).toInt)
})
import spark.implicits._
spark.createDataset(rdd).show(false)
}
}
DataSet 转换为RDD
调用rdd方法即可。
1)创建一个DataSet
scala> val DS = Seq(Person("Andy", 32)).toDS()
DS: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
2)将DataSet转换为RDD
scala> DS.rdd
res11: org.apache.spark.rdd.RDD[Person] = MapPartitionsRDD[15] at rdd at <console>:28
DataFrame与DataSet的互操作
DataSet转DataFrame
这个很简单理解,因为只是把case class封装成Row。
1)导入隐式转换
import spark.implicits._
(2)转换
val testDF = testDS.toDF
DataFrame转DataSet
(1)导入隐式转换
import spark.implicits._
(2)创建样例类
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
(3)转换
val testDS = testDF.as[Coltest]
这种方法就是在给出每一列的类型后,使用as方法,转成Dataset,这在数据类型是DataFrame又需要针对各个字段处理时极为方便。在使用一些特殊的操作时,一定要加上 import spark.implicits._
不然toDF、toDS无法使用。
RDD、DataFrame、DataSet
在SparkSQL中Spark为我们提供了两个新的抽象,分别是DataFrame和DataSet。他们和RDD有什么区别呢?首先从版本的产生上来看:
RDD (Spark1.0) —> Dataframe(Spark1.3) —> Dataset(Spark1.6)
如果同样的数据都给到这三个数据结构,他们分别计算之后,都会给出相同的结果。不同是的他们的执行效率和执行方式。
在后期的Spark版本中,DataSet会逐步取代RDD和DataFrame成为唯一的API接口。
三者的共性
-
RDD、DataFrame、Dataset 全都是spark平台下的分布式弹性数据集,为处理超大型数据提供便利。
-
三者都有惰性机制,在进行创建、转换,如map方法时,不会立即执行,只有在遇到Action(行动算子)如foreach时,三者才会开始遍历运算。
-
三者都会根据spark的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出。
-
三者都有partition的概念
-
三者有许多共同的函数,如filter,排序等
-
在对DataFrame和Dataset进行操作许多操作都需要这个包进行支持
import spark.implicits._
-
DataFrame和Dataset均可使用模式匹配获取各个字段的值和类型
例如:
DataFrame:
testDF.map{ case Row(col1:String,col2:Int)=> println(col1);println(col2) col1 case _=> "" }
Dataset:
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型 testDS.map{ case Coltest(col1:String,col2:Int)=> println(col1);println(col2) col1 case _=> "" }
三者的区别
1. RDD:
-
RDD一般和spark mlib(机器学习库)同时使用
-
RDD不支持sparksql操作
2. DataFrame:
-
与RDD和Dataset不同,DataFrame每一行的类型固定为Row,每一列的值没法直接访问,只有通过解析才能获取各个字段的值
testDF.foreach{ line => val col1=line.getAs[String]("col1") val col2=line.getAs[String]("col2") }
-
DataFrame与Dataset一般不与spark mlib同时使用
-
DataFrame与Dataset均支持sparksql的操作,比如select,groupby之类,还能注册临时表/视窗,进行sql语句操作
dataDF.createOrReplaceTempView("tmp") spark.sql("select ROW,DATE from tmp where DATE is not null order by DATE").show(100,false)
-
DataFrame与Dataset支持一些特别方便的保存方式,比如保存成csv,可以带上表头,这样每一列的字段名一目了然
//保存 val saveoptions = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://hadoop102:9000/test") datawDF.write.format("com.atguigu.spark.csv").mode(SaveMode.Overwrite).options(saveoptions).save() //读取 val options = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://hadoop102:9000/test") val datarDF= spark.read.options(options).format("com.atguigu.spark.csv").load()
3. Dataset:
- Dataset和DataFrame拥有完全相同的成员函数,区别只是每一行的数据类型不同
- DataFrame也可以叫Dataset[Row],每一行的类型是Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用上面提到的getAS方法或者共性中的第七条提到的模式匹配拿出特定字段。而Dataset中,每一行是什么类型是不一定的,在自定义了case class之后可以很自由的获得每一行的信息
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
/**
rdd
("a", 1)
("b", 1)
("a", 1)
**/
val test: Dataset[Coltest]=rdd.map{line=>
Coltest(line._1,line._2)
}.toDS
test.map{
line=>
println(line.col1)
println(line.col2)
}
可以看出,Dataset在需要访问列中的某个字段时是非常方便的,然而,如果要写一些适配性很强的函数时,如果使用Dataset,行的类型又不确定,可能是各种case class,无法实现适配,这时候用DataFrame即Dataset[Row]就能比较好的解决问题。
转化总结
关于RDD、DataFrame、DataSet之间如何相互转换,上面已经说过了,下面用一张图总结一下。
案列
所有交易的周分布情况 :
import java.text.SimpleDateFormat
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.Calendar
import org.apache.spark.sql.SparkSession
// 手动导包
import org.apache.spark.sql.functions._
object MyDay {
def dayChange(datestr:String)={
val arr = Array("日","一","二","三","四","五","六")
val cal = Calendar.getInstance()
cal.setTime(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(datestr))
arr(cal.get(Calendar.DAY_OF_WEEK)-1)
}
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local[*]").appName("test").getOrCreate()
val df = spark.read.format("csv").load("dfdata/orders.csv")
.toDF("orderId","orddate","userId","status")
val cs = spark.read.format("csv").load("dfdata/customers.csv")
.toDF("userId","lname","fname","pwd1","pwd2","address","city","country","zip")
val item = spark.read.format("csv").load("dfdata/order_items.csv")
.toDF("id","orderId","productId","buyNum","countPrice","price")
// 所有交易的周分布情况
// 方法一:SQL语句操作+hive Sql函数
df.createOrReplaceTempView("orders")
spark.sql("""select
|case dayofweek(orddate) when 1 then "Sunday" when 2 then "Monday"
|when 3 then "Tuesday" when 4 then "wednesday" when 5 then "Tuesday"
|when 6 then "Friday" else "Saturday" end weekday,count(*)
|from orders group by dayofweek(orddate)""".stripMargin).show(false)
// 方法二 (最常用):自定义UDF+df算子操作
import spark.implicits._
// 自定义UDF
val wdChange = udf{num:Int=>
num match {
case 1 => "Sunday"
case 2 => "Monday"
case 3 => "Tuesday"
case 4 => "wednesday"
case 5 => "Tuesday"
case 6 => "Friday"
case _ => "Saturday"
}
}
df.groupBy(dayofweek($"orddate").as("weekday"))
.agg(count("orderId").alias("countid"))
.withColumn("wd",wdChange($"weekday")).drop("weekday")
.show(false)
// 方法三:DF算子+LocalDate库操作
df.select("orderId","orddate").map(f=>(
LocalDate.parse(f(1).toString,
DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")).getDayOfWeek.getValue,f(0).toString))
.rdd.groupByKey().foreach(x=>println(x._1,x._2.size))
// 方法四:DF算子+自定义函数dayChange操作
df.select("orderId","orddate").map(f=>(dayChange(f(1).toString),f(0).toString))
.rdd.groupByKey().foreach(x=>println(x._1,x._2.size))
df.select("orderId","orddate").map(f=>(dayChange(f(1).toString),f(0).toString))
.groupBy("_1").agg("_2"->"count").show(false)
}
}
SparkSession 数据读取与保存
Spark的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。文件格式分为:Text文件、Json文件、Csv文件、Sequence文件以及Object文件;文件系统分为:本地文件系统、HDFS、Hive以及数据库。
文件类数据读取与保存
Text文件
1)数据读取:textFile(String)
scala> val hdfsFile = sc.textFile("hdfs://vmsingle:9000/fruit.txt")
hdfsFile: org.apache.spark.rdd.RDD[String] = hdfs://vmsingle:9000/fruit.txt MapPartitionsRDD[21] at textFile at <console>:24
2)数据保存: saveAsTextFile(String)
scala> hdfsFile.saveAsTextFile("/fruitOut")
Json文件
如果JSON文件中每一行就是一个JSON记录,那么可以通过将JSON文件当做文本文件来读取,然后利用相关的JSON库对每一条数据进行JSON解析。
注意:使用RDD读取JSON文件处理很复杂,同时SparkSQL集成了很好的处理JSON文件的方式,所以应用中多是采用SparkSQL处理JSON文件。
json.txt 源数据:
{"name":"hubert","age":"100"}
{"name":"rose","age":"10"}
{"name":"jack","age":"39"}
{"name":"henry","age":"34"}
textFile读取json文件:
val conf = new SparkConf().setMaster("local[*]").setAppName("test")
val sc = new SparkContext(conf)
// 导入解析json所需的包
import scala.util.parsing.json.JSON
// 读取json文件
val rdd = sc.textFile("data/json.txt")
// 解析json数据
val result = rdd.map(line=>JSON.parseFull(line))
// 操作
result.foreach(f=>println(f.get.asInstanceOf[Map[String,String]].get("name").get))
sc.stop()
// 结果
hubert
jack
rose
henry
read.format 读取json文件:
val spark = SparkSession.builder().appName("test").master("local[*]").getOrCreate()
// 读取json文件
val df = spark.read.format("json").load("data/json.txt")
// 操作
df.select("name","age").show(false)
spark.close()
// 结果
+------+---+
|name |age|
+------+---+
|hubert|100|
|rose |10 |
|jack |39 |
|henry |34 |
+------+---+
Sequence文件
SequenceFile文件是Hadoop用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)。Spark 有专门用来读取 SequenceFile 的接口。在 SparkContext 中,可以调用 sequenceFile[ keyClass, valueClass](path)。
注意:SequenceFile文件只针对PairRDD
1)创建一个RDD
scala> val rdd = sc.parallelize(Array((1,2),(3,4),(5,6)))
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[13] at parallelize at <console>:24
2)将RDD保存为Sequence文件
scala> rdd.saveAsSequenceFile("file:///opt/spark234/seqFile")
3)查看该文件
[root@vmsingle seqFile]# pwd
/opt/spark234/seqFile
[root@vmsingle seqFile]# ll
总用量 8
-rw-r--r-- 1 root root 108 10月 9 10:29 part-00000
-rw-r--r-- 1 root root 124 10月 9 10:29 part-00001
-rw-r--r-- 1 root root 0 10月 9 10:29 _SUCCESS
[root@vmsingle seqFile]# cat part-00000
4)读取Sequence文件
scala> val seq = sc.sequenceFile[Int,Int]("file:///opt/spark234/seqFile")
seq: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[18] at sequenceFile at <console>:24
5)打印读取后的Sequence文件
scala> seq.collect
res14: Array[(Int, Int)] = Array((1,2), (3,4), (5,6))
对象文件
对象文件是将对象序列化后保存的文件,采用Java的序列化机制。可以通过objectFile[k,v](path) 函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用saveAsObjectFile() 实现对对象文件的输出。因为是序列化所以要指定类型。
1)创建一个RDD
scala> val rdd = sc.parallelize(Array(1,2,3,4))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[19] at parallelize at <console>:24
2)将RDD保存为Object文件
scala> rdd.saveAsObjectFile("file:///opt/spark234/objectFile")
3)查看该文件
[root@vmsingle seqFile]# pwd
/opt/spark234/objectFile
[root@vmsingle seqFile]# ll
总用量 8
-rw-r--r-- 1 atguigu atguigu 142 10月 9 10:37 part-00000
-rw-r--r-- 1 atguigu atguigu 142 10月 9 10:37 part-00001
-rw-r--r-- 1 atguigu atguigu 0 10月 9 10:37 _SUCCESS
[root@vmsingle seqFile]# cat part-00000
SEQ!org.apache.hadoop.io.NullWritable"org.apache.hadoop.io.BytesWritableW@`l
4)读取Object文件
scala> val objFile = sc.objectFile[Int]("file:///opt/spark234/objectFile")
objFile: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[31] at objectFile at <console>:24
5)打印读取后的Object文件
scala> objFile.collect
res19: Array[Int] = Array(1, 2, 3, 4)
总结
-
SparkSQL 写数据:
DataFrame/DataSet.write.json/csv/jdbc
-
SparkSQL读数据:
SparkSession.read.json/csv/text/jdbc/format
文件系统类数据读取与保存
HDFS
Spark的整个生态系统与Hadoop是完全兼容的,所以对于Hadoop所支持的文件类型或者数据库类型,Spark也同样支持。另外,由于Hadoop的API有新旧两个版本,所以Spark为了能够兼容Hadoop所有的版本,也提供了两套创建操作接口。对于外部存储创建操作而言,hadoopRDD和newHadoopRDD是最为抽象的两个函数接口,主要包含以下四个参数。
1)输入格式(InputFormat): 制定数据输入的类型,如TextInputFormat等,新旧两个版本所引用的版本分别是org.apache.hadoop.mapred.InputFormat
和org.apache.hadoop.mapreduce.InputFormat(NewInputFormat)
2)键类型: 指定[K,V]键值对中K的类型
3)值类型: 指定[K,V]键值对中V的类型
4)分区值: 指定由外部存储生成的RDD的partition数量的最小值,如果没有指定,系统会使用默认值defaultMinSplits
。
注意:其他创建操作的API接口都是为了方便最终的Spark程序开发者而设置的,是这两个接口的高效实现版本。例如,对于textFile而言,只有path这个指定文件路径的参数,其他参数在系统内部指定了默认值。
-
在Hadoop中以压缩形式存储的数据,不需要指定解压方式就能够进行读取,因为Hadoop本身有一个解压器会根据压缩文件的后缀推断解压算法进行解压。
-
如果用Spark从Hadoop中读取某种类型的数据不知道怎么读取的时候,上网查找一个使用map-reduce的时候是怎么读取这种这种数据的,然后再将对应的读取方式改写成上面的hadoopRDD和newAPIHadoopRDD两个类就行了。
MySQL数据库连接
支持通过Java JDBC访问关系型数据库。需要通过JdbcRDD进行,示例如下:
(1)添加依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
(2)mysql读写操作
object LinkSql {
def main(args: Array[String]): Unit = {
// 1.创建sparkSession 对象
val spark = SparkSession.builder().appName("test").master("local[*]").getOrCreate()
// 设置连接用户、密码、数据库驱动类
val prop = new Properties()
prop.setProperty("driver","com.mysql.jdbc.Driver")
prop.setProperty("user","root")
prop.setProperty("password","hubert")
// 读取库hubert的表games的数据
val jdbcDF = spark.read.jdbc("jdbc:mysql://192.168.152.140:3306/hubert","games",prop)
jdbcDF.show(false)
// 创建DF
val df = spark.createDataFrame(spark.sparkContext.parallelize(
Seq((4,"地下城","电竞网游","拳头","2007"), (5,"刀塔","电竞网游","拳头","2007")
))).toDF("gameId","gameName","gameType","gameCompany","gameYear")
df.show(false)
// DF写入库hubert的表games的数据
df.write.mode("append").jdbc("jdbc:mysql://192.168.152.140:3306/hubert","games",prop)
}
}
Hive 连接
Spark on hive 与 hive on spark 的区别
-
Spark on hive
是spark 通过Spark-SQL使用hive 语句,操作hive ,底层运行的还是 spark rdd。
(1)就是通过sparksql,加载hive的配置文件,获取到hive的元数据信息
(2)spark sql获取到hive的元数据信息之后就可以拿到hive的所有表的数据
(3)接下来就可以通过spark sql来操作hive表中的数据
-
hive on spark
是把hive查询从mapreduce 的mr (Hadoop计算引擎)操作替换为spark rdd(spark 执行引擎) 操作. 相对于spark on hive,这个要实现起来则麻烦很多, 必须重新编译你的spark和导入jar包,不过目前大部分使用的是spark on hive。
Hive查询流程及原理
-
执行HQL时,先到MySQL元数据库中查找描述信息,然后解析HQL并根据描述信息生成MR任务
-
Hive将SQL转成MapReduce执行速度慢
-
使用SparkSQL整合Hive其实就是让SparkSQL去加载Hive 的元数据库,然后通过SparkSQL执行引擎去操作Hive表内的数据
-
首先需要开启Hive的元数据库服务,让SparkSQL能够加载元数据
(1)添加依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-hive_2.11</artifactId>
<version>2.3.4</version>
</dependency>
(2) 加载配置文件
Spark 有一个内置的 MateStore,使用 Derby 嵌入式数据库保存数据,但是这种方式不适合生产环境,因为这种模式同一时间只能有一个 SparkSession 使用,所以生产环境更推荐使用 Hive 的 MetaStore。
SparkSQL 整合 Hive 的 MetaStore 主要思路就是要通过配置能够访问它, 并且能够使用 HDFS 保存 WareHouse,所以可以直接拷贝 Hadoop 和 Hive 的配置文件到 Spark 的配置目录
- hive-site.xml 元数据仓库的位置等信息
- core-site.xml 安全相关的配置
- hdfs-site.xml HDFS 相关的配置
(3) 操作hive
object LinkHive {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local[*]").appName("test").enableHiveSupport().getOrCreate()
val df = spark.sql("select * from hubert.userinfos")
df.show(false)
}
}
Parquet
Parquet文件是一种流行的列式存储格式,以二进制存储,文件中包含数据与元数据。
object LinkParquet {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local[*]").appName("test").enableHiveSupport().getOrCreate()
val sc = spark.sparkContext
//Spark SQL写parquet文件
import org.apache.spark.sql.types.{StructType, StructField, StringType,ArrayType,IntegerType}
val schema=StructType(Array(StructField("name",StringType),
StructField("favorite_color",StringType),
StructField("favorite_numbers",ArrayType(IntegerType))))
val rdd=sc.parallelize(List(("Alyssa",null,Array(3,9,15,20)),("Ben","red",null)))
val rowRDD=rdd.map(p=>Row(p._1,p._2,p._3))
val df=spark.createDataFrame(rowRDD,schema)
//在该目录下生成parquet文件
df.write.parquet("/data/users")
//Spark SQL读parquet文件(该目录下存在parquet文件)
val df1=spark.read.parquet("/data/users")
df1.show
df1.printSchema
}
}
结果: