第1章 Spark SQL概述
什麼是Spark SQL
Spark SQL是Spark用來處理結構化數據
的一個模塊,它提供了2個編程抽象:DataFrame
和DataSet
,來作爲分佈式SQL查詢
的引擎。
我們已經學習了Hive,它是將Hive SQL轉換成MapReduce然後提交到集羣上執行,大大簡化了編寫MapReduc的程序的複雜性,由於MapReduce這種計算模型執行效率比較慢。所有Spark SQL的應運而生,它是將Spark SQL轉換成RDD
,然後提交到集羣執行,執行效率非常快
!
傳統的數據分析中一般無非就是SQL
,跟MapReduce
。但是MapReduce
的八股文書寫方式太煩人了,所以引入了依靠MapReduce
引擎建設出來的Hive
,Spark爲了融合Hive也推出了Shark
。同時Spark模仿Hive的框架形成了SparkSQL。開發敏捷性,執行速度。
Spark SQL的特點
- 易整合
- 統一的數據訪問方式
- 兼容Hive
- 標準的數據連接
什麼是DataFrame
在Spark中,DataFrame是一種以RDD爲基礎的分佈式數據集
,類似於傳統數據庫中的二維表格
。DataFrame
與RDD
的主要區別在於,前者帶有schema元信息
,即DataFrame
所表示的二維表數據集的每一列都帶有名稱
和類型
。這使得Spark SQL
得以洞察更多的結構信息,從而對藏於DataFrame背後的數據源以及作用於DataFrame之上的變換進行了針對性的優化,最終達到大幅提升運行時效率的目標。反觀RDD,由於無從得知所存數據元素的具體內部結構,Spark Core只能在stage層面進行簡單、通用的流水線優化。
DataFrame
也是懶執行
的,但性能上比RDD要高
,主要原因:
優化
的執行計劃,即查詢計劃通過Spark catalyst optimiser
進行優化。比如下面一個例子:
SQL解析成RDD編程,系統執行一般比人寫的更好些。
val rdd1 = sc.makeRDD(List((1,"a"),(2,"b"),(3,"c")))
val rdd2 = sc.makeRDD(List((1,"1"),(2,"2"),(3,"3")))
自己寫的話 笛卡爾乘積先出來然後過濾
rdd1.join(rdd2).filter{
case (key,(v1,v2)=>{
key == 1
})
}
sparksql
select * from t_table1 a join t_table2 b on a.x = b.x where a.id = 1
底層是 先過濾再笛卡爾乘積,若干底層優化。
rdd1.filter(xxx) ==> 1
join
rdd2.filter(xxx) ==> 1
什麼是DataSet
DataSet
是分佈式數據集合
。DataSet
是Spark 1.6中添加的一個新抽象,是DataFrame的一個擴展
。類似與ORM,它提供了RDD的優勢(強類型,使用強大的lambda函數的能力)以及Spark SQ
L優化執行引擎的優點。DataSet也可以使用功能性的轉換(操作map,flatMap,filter等等)。
- 是DataFrame API的一個
擴展
,是SparkSQL最新的數據抽象; - 用戶友好的API風格,既具有類型安全檢查也具有DataFrame的查詢優化特性;
- 用樣例類來對DataSet中定義數據的結構信息,樣例類中每個屬性的名稱直接映射到DataSet中的字段名稱;
- DataSet是
強
類型的。比如可以有DataSet[Car],DataSet[Person]。
三者區別:
單純的RDD只有KV這樣的數據沒有結構,給RDD的數據增加若干結構
形成了DataFrame
,而爲了訪問方便不再像SQL那樣獲取第幾個數據,而是像讀取對象
那種方式而催生出了DataSet
。
第二章 SparkSQL編程
1. SparkSession新的起始點
在老的版本中,SparkSQL提供兩種SQL查詢起始點:一個叫SQLContext
,用於Spark自己提供的SQL查詢;一個叫HiveContext
,用於連接Hive的查詢。
SparkSession
是Spark最新的SQL查詢起始點,實質上是SQLContext和HiveContext的組合
,所以在SQLContext
和HiveContext
上可用的API在SparkSession
上同樣是可以使用的。SparkSession
內部封裝了sparkContext
,所以計算實際上是由sparkContext
完成的
2. DataFrame
創建
在Spark SQL中SparkSession
是創建DataFrame和執行SQL的入口,創建DataFrame有三種方式:通過Spark的數據源進行創建
;從一個存在的RDD進行轉換
;還可以從Hive Table進行查詢
返回。
從Spark數據源進行創建
- 查看Spark數據源進行創建的文件格式
scala> spark.read.
csv format jdbc json load option options orc parquet schema table text textFile
- 讀取json文件創建DataFrame
scala> val df = spark.read.json("/opt//people.json") //讀取分區上的文件,本地用 file:///pwd
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
- 展示結果
scala> df.show
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
SQL風格語法(主要)
- 創建一個DataFrame
scala> val df = spark.read.json("/opt/module/spark/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
- 對DataFrame創建一個臨時表,View是隻讀的,Table有改的意思哦。
scala> df.createOrReplaceTempView("people")
- 通過SQL語句實現查詢全表
scala> val sqlDF = spark.sql("SELECT * FROM people")
sqlDF: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
---
scala> val del = spark.sql("drop table if exists stu")
del: org.apache.spark.sql.DataFrame = []
- 結果展示
scala> sqlDF.show
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
注意
:普通臨時表是Session
範圍內的,如果想應用範圍內有效,可以使用全局臨時表。使用全局臨時表時需要全路徑訪問
,如:global_temp.people
5. 對於DataFrame創建一個全局表
scala> df.createGlobalTempView("people")
- 通過SQL語句實現查詢全表
scala> spark.sql("SELECT * FROM global_temp.people").show()
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
創建新session
scala> spark.newSession().sql("SELECT * FROM global_temp.people").show()
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
DSL風格語法(次要)
- 創建一個DataFrame
scala> val df = spark.read.json("/opt/module/spark/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
- 查看DataFrame的Schema信息
scala> df.printSchema
root
|-- age: long (nullable = true)
|-- name: string (nullable = true)
- 只查看”name”列數據
scala> df.select("name").show()
+-------+
| name|
+-------+
|Michael|
| Andy|
| Justin|
+-------+
- 查看”name”列數據以及”age+1”數據
scala> df.select($"name", $"age" + 1).show()
+-------+---------+
| name|(age + 1)|
+-------+---------+
|Michael| null|
| Andy| 31|
| Justin| 20|
+-------+---------+
- 查看”age”大於”21”的數據
scala> df.filter($"age" > 21).show()
+---+----+
|age|name|
+---+----+
| 30|Andy|
+---+----+
- 按照”age”分組,查看數據條數
scala> df.groupBy("age").count().show()
+----+-----+
| age|count|
+----+-----+
| 19| 1|
|null| 1|
| 30| 1|
+----+-----+
RDD轉換爲DataFrame
注意
:如果需要RDD與DF或者DS之間操作,那麼都需要引入 import spark.implicits._ (spark不是包名,而是sparkSession對象的名稱)
前置條件:導入隱式轉換並創建一個RDD
1. 手動轉換
scala>
import spark.implicits._
scala> val peopleRDD = sc.textFile("examples/src/main/resources/people.txt")
peopleRDD: org.apache.spark.rdd.RDD[String] = examples/src/main/resources/people.txt MapPartitionsRDD[3] at textFile at <console>:27
- 通過手動確定轉換
scala> peopleRDD.map{x=>val para = x.split(",");(para(0),para(1).trim.toInt)}.toDF("name","age")
res1: org.apache.spark.sql.DataFrame = [name: string, age: int]
2. 通過反射確定(需要用到樣例類)
- 創建一個樣例類
scala> case class People(name:String, age:Int)
- 根據樣例類將RDD轉換爲DataFrame
scala> peopleRDD.map{ x => val para = x.split(",");People(para(0),para(1).trim.toInt)}.toDF
res2: org.apache.spark.sql.DataFrame = [name: string, age: int]
peopleRDD.map(x=>{People(x._1,x._2)}).toDF
3. 通過編程的方式(瞭解)
- 導入所需的類型
scala> import org.apache.spark.sql.types._
import org.apache.spark.sql.types._
- 創建Schema
scala> val structType: StructType = StructType(StructField("name", StringType) :: StructField("age", IntegerType) :: Nil)
structType: org.apache.spark.sql.types.StructType = StructType(StructField(name,StringType,true), StructField(age,IntegerType,true))
- 導入所需的類型
scala> import org.apache.spark.sql.Row
import org.apache.spark.sql.Row
- 根據給定的類型創建二元組RDD
scala> val data = peopleRDD.map{ x => val para = x.split(",");Row(para(0),para(1).trim.toInt)}
data: org.apache.spark.rdd.RDD[org.apache.spark.sql.Row] = MapPartitionsRDD[6] at map at <console>:33
- 根據數據及給定的schema創建DataFrame
scala> val dataFrame = spark.createDataFrame(data, structType)
dataFrame: org.apache.spark.sql.DataFrame = [name: string, age: int]
DataFrame轉換爲RDD
直接調用rdd
即可
- 創建一個DataFrame
scala> val df = spark.read.json("/opt/module/spark/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
- 將DataFrame轉換爲RDD
scala> val dfToRDD = df.rdd
dfToRDD: org.apache.spark.rdd.RDD[org.apache.spark.sql.Row] = MapPartitionsRDD[19] at rdd at <console>:29
DataFrame 關心的是行,所以轉換的時候是按照行來轉換的
- 打印RDD
scala> dfToRDD.collect
res13: Array[org.apache.spark.sql.Row] = Array([Michael, 29], [Andy, 30], [Justin, 19])
注意
:返回的類型都是org.apache.spark.sql.Row
3. DataSet
DataSet是具有強類型的數據集合,需要提供對應的類型信息。
創建
- 創建一個樣例類
scala> case class Person(name: String, age: Long)
defined class Person
- 創建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類可以包含諸如Seqs或者Array等複雜的結構。
- 創建一個RDD
scala> val peopleRDD = sc.textFile("examples/src/main/resources/people.txt")
peopleRDD: org.apache.spark.rdd.RDD[String] = examples/src/main/resources/people.txt MapPartitionsRDD[3] at textFile at <console>:27
- 創建一個樣例類
scala> case class Person(name: String, age: Long)
defined class Person
- 將RDD轉化爲DataSet
scala> peopleRDD.map(line => {val para = line.split(",");Person(para(0),para(1).trim.toInt)}).toDS
res8: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
DataSet轉換爲RDD
調用rdd方法即可。
- 創建一個DataSet
scala> val DS = Seq(Person("Andy", 32)).toDS()
DS: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
- 將DataSet轉換爲RDD
scala> DS.rdd
res11: org.apache.spark.rdd.RDD[Person] = MapPartitionsRDD[15] at rdd at <console>:28
4. DataFrame與DataSet的互操作
DataFrame轉DataSet
- 創建一個DateFrame
scala> val df = spark.read.json("examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
- 創建一個樣例類
scala> case class Person(name: String, age: Long)
defined class Person
- 將DataFrame轉化爲DataSet,添加類型
scala> df.as[Person]
res14: org.apache.spark.sql.Dataset[Person] = [age: bigint, name: string]
Dataset轉DataFrame
- 創建一個樣例類
scala> case class Person(name: String, age: Long)
defined class Person
- 創建DataSet
scala> val ds = Seq(Person("Andy", 32)).toDS()
ds: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
- 將DataSet轉化爲DataFrame
scala> val df = ds.toDF
df: org.apache.spark.sql.DataFrame = [name: string, age: bigint]
- 展示
scala> df.show
+----+---+
|name|age|
+----+---+
|Andy| 32|
+----+---+
這種方法就是在給出每一列的類型後,使用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會逐步取代另外兩者稱爲唯一接口。
所以在做一個整體的項目時候,一般還是以Java爲主,只有在涉及到迭代式計算採用到Scala這樣到函數式編程。
相同點
- RDD、DataFrame、DataSet全部都是平臺下到
分佈式彈性數據集
,爲處理超大型數據提供了便利 - 三者都有
惰性機制
,在創建,轉換,如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 _ => " "
}
不同點
- RDD:
- RDD 一般跟sparkMlib 同時使用
- RDD 不支持sparkSQL操作
- DataFrame
- 跟RDD和DataSet不同,DataFrame 每一行類型都固定爲Row,每一列值無法直接訪問,只有通過解析纔可以獲得各個字段。
testDf.foreach{
line=>
val col1 = line.getAs[String]("col1")
val col2 = line.getAs[String]("col2")
}
- DataFrame跟DataSet一般不跟sparkMlib共同使用。
- 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,可以帶表頭,每一列字段一目瞭然。這樣的保存方式可以方便的獲得字段名跟列的對應,而且分隔符(delimiter)可自定義
val saveoptions = Map("header"->"true","delimiter"->"\t","path"->"hdfs://hadoop102:9000/test")
val dataDF = spark.read.options(options).format("com.sowhat.spark.csv").load()
- DataSet
- DataSet 跟DataFrame擁有完全一樣的成員函數,唯一區別就是每一行數據類型不同。
- DataFrame也可以叫DataSet[Row],每一行類型都是Row,不解析每一行究竟有那些字段,每個字段又是什麼類型無從得知,只能通上面提到的
getAs
方法或者共性的第七條的模式匹配來拿出特定的字段,而DataSet中每一行是什麼類型是不一定的,在自定義了case class 之後可以自由獲得每一行信息。
case class Coltest(col1:String,col2:Int) extends Serializable
//定義字段名跟類型
val test:DataSet[Coltest] = rdd.map{
Coltest(line._1,line_2)
}.toDS
test.map{
line=>
println(line.col1)
println(line.col2)
}
可以看出,DataSet在需要訪問列中的某個字段時候非常方便,然而如果要寫一些是適配性極強的函數時候,如果使用DataSet,行的類型又不確定,可能是各自case class,無法實現適配,這時候可以用DataFrame 既DataSet[Row]很好的解決問題。
IDEA 創建SparkSQL
引入依賴
<dependencies>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>2.1.1</version>
</dependency>
<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>
</dependencies>
入門demo
package com.sowhat.udaf
import org.apache.spark.sql.{DataFrame, SparkSession}
object TestCustomerAvg {
def main(args: Array[String]): Unit = {
//1.創建SparkSession
val spark: SparkSession = SparkSession
.builder()
.master("local[*]")
.appName("WordCount")
.getOrCreate()
//2.導入隱式轉換
import spark.implicits._
//3.讀取文件創建DF
val df: DataFrame = spark.read.json("/Users/liujinjie/Downloads/Spark1015/SparkSQL/src/data/people.json")
//4.創建一張臨時表
df.createTempView("people")
spark.sql("select * from people").show
//7.關閉連接
spark.stop()
}
}
RDDDFDS 轉換
package com.sowhat.test
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}
object RDDToDF {
def main(args: Array[String]): Unit = {
//1.創建SparkSession
val spark: SparkSession = SparkSession
.builder()
.master("local[*]")
.appName("RDDToDF")
.getOrCreate()
//2.導入隱式轉換
import spark.implicits._
//3.創建RDD
val rdd: RDD[(Int, String, Int)] = spark.sparkContext.makeRDD(List((1, "zhang", 20), (2, "san", 30), (3, "si", 40)))
val value: RDD[User] = rdd.map {
case (id, name, age) => User(id, name, age)
}
val userDS: Dataset[User] = value.toDS()
val rdd2: RDD[User] = userDS.rdd
rdd2.foreach(println)
// 轉換爲DF
val df: DataFrame = rdd.toDF("id", "name", "age")
// 轉換爲DS
val ds: Dataset[User] = df.as[User]
// 轉換爲DF
val df1: DataFrame = ds.toDF()
// 轉換爲RDD
val rdd1: RDD[Row] = df1.rdd
rdd1.foreach(row => {
// 這個是數據的索引
println(row.getString(1))
})
//8.關閉連接
spark.stop()
}
}
case class User(id: Int, name: String, age: Int)
建議SparkSQL開發儘量下面三行直接寫好
val sparkconf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("demo")
val spark: SparkSession = SparkSession.builder().config(sparkconf).getOrCreate()
// 進行轉換前 需要引入隱式轉換規則,這裏引入的是SparkSession 對象名字
import spark.implicits._
用戶自定義函數
在Shell窗口中可以通過spark.udf功能用戶可以自定義函數。
UDF
- 創建DataFrame
scala> val df = spark.read.json("examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
- 打印數據
scala> df.show()
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
- 註冊UDF,功能爲在數據前添加字符串
scala> spark.udf.register("addName", (x:String)=> "Name:"+x)
res5: 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|
+-----------------+----+
UDAF
強
類型的Dataset和弱
類型的DataFrame都提供了相關的聚合函數, 如 count(),countDistinct(),avg(),max(),min()。除此之外,用戶可以設定自己的自定義聚合函數。通過繼承UserDefinedAggregateFunction來實現用戶自定義聚合函數。
需求
:實現求平均工資的自定義聚合函數。
people.json
{"name":"Michael","age": 21}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19}
弱類型實現
依據DataFrame
類型的查詢數據,只能通過索引形式找到數據,必須記住自己的數據對應的索引位置。注意導入正確的package !
自定義若類型函數
package com.atguigu.udaf
import org.apache.spark.sql.Row
import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction}
import org.apache.spark.sql.types._
// 數據類型均跟SparkSQL相關的類型
object CustomerAvg extends UserDefinedAggregateFunction {
//輸入數據類型
override def inputSchema: StructType = StructType(StructField("input", LongType) :: Nil)
//緩存數據的類型
override def bufferSchema: StructType = StructType(StructField("sum", LongType ) :: StructField("count", LongType) :: Nil)
//輸出數據類型
override def dataType: DataType = DoubleType
//函數確定性
override def deterministic: Boolean = true
// 計算前 緩衝區 初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer(0) = 0L
buffer(1) = 0L
}
//分區內 數據 更新
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
if (!input.isNullAt(0)) {
buffer(0) = buffer.getLong(0) + input.getLong(0)
buffer(1) = buffer.getLong(1) + 1L
}
}
//多個節點多緩衝區 合併值
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0)
buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)
}
//計算最終結果
override def evaluate(buffer: Row): Any = {
buffer.getLong(0).toDouble / buffer.getLong(1)
}
}
調用
package com.atguigu.udaf
import org.apache.spark.sql.{DataFrame, Dataset, SparkSession, TypedColumn}
object TestCustomerAvg {
def main(args: Array[String]): Unit = {
//1.創建SparkSession
val spark: SparkSession = SparkSession
.builder()
.master("local[*]")
.appName("WordCount")
.getOrCreate()
//2.導入隱式轉換
import spark.implicits._
//3.讀取文件創建DF
val df: DataFrame = spark.read.json("/Users/liujinjie/Downloads/Spark1015/SparkSQL/src/data/people.json")
//4.創建一張臨時表
df.createTempView("people")
//5.註冊函數
spark.udf.register("MyAvg", CustomerAvg)
//6.使用UDAF
spark.sql("select MyAvg(age) as sqlAge from people").show
//創建聚合對象
val udaf = new MyAgeAvgClassFunction
// 將聚合函數查詢轉換爲查詢列
val avgCol: TypedColumn[UserBean, Double] = udaf.toColumn.name("avgAge")
val userDS: Dataset[UserBean] = df.as[UserBean]
userDS.select(avgCol).show()
//7.關閉連接
spark.stop()
}
}
強類型實現
強類型無法使用SQL形式查詢調用函數,只能用DSL風格。
自定義函數
package com.atguigu.spark
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Encoder, Encoders, SparkSession}
// 既然是強類型,可能有case類
case class Employee(name: String, salary: Long)
case class Average(var sum: Long, var count: Long)
class MyAverage extends Aggregator[Employee, Average, Double] {
// 定義一個數據結構,保存工資總數和工資總個數,初始都爲0
def zero: Average = Average(0L, 0L)
// 聚合相同executor分片中的結果
def reduce(buffer: Average, employee: Employee): Average = {
buffer.sum += employee.salary
buffer.count += 1
buffer
}
// 聚合不同execute的結果
def merge(b1: Average, b2: Average): Average = {
b1.sum += b2.sum
b1.count += b2.count
b1
}
// 計算輸出
def finish(reduction: Average): Double = reduction.sum.toDouble / reduction.count
// 設定中間值類型的編碼器,要轉換成case類
// Encoders.product是進行scala元組和case類轉換的編碼器
def bufferEncoder: Encoder[Average] = Encoders.product
// 設定最終輸出值的編碼器
def outputEncoder: Encoder[Double] = Encoders.scalaDouble
}
object MyAverage{
def main(args: Array[String]) {
//創建SparkConf()並設置App名稱
val spark = SparkSession
.builder()
.appName("sowhat")
.master("local[4]")
.config("spark.testing.memory", "471859200")
.getOrCreate()
import spark.implicits._
// For implicit conversions like converting RDDs to DataFrames
val ds = spark.read.json("employees.json").as[Employee]
ds.show()
val averageSalary = new MyAverage().toColumn.name("average_salary")
val result = ds.select(averageSalary)
result.show()
spark.stop()
}
}
/**
* {"name":"Michael", "salary":3000}
* {"name":"Andy", "salary":4500}
* {"name":"Justin", "salary":3500}
* {"name":"Berta", "salary":4000}
*
* */
第三章 Spark SQL數據的加載與保存
通用加載/保存方法
1. 加載數據
- read直接加載數據
scala> spark.read.
csv jdbc json orc parquet textFile… …
注意
:加載數據的相關參數需寫到上述方法中。如:textFile需傳入加載數據的路徑,jdbc需傳入JDBC相關參數。
2. format指定加載數據類型
scala> spark.read.format("…")[.option("…")].load("…")
用法詳解
:
3. format("…"):指定加載的數據類型,包括"csv"、“jdbc”、“json”、“orc”、“parquet"和"textFile”。
4. load("…"):在"csv"、“orc”、“parquet"和"textFile"格式下需要傳入加載數據的路徑。
5. option(”…"):在"jdbc"格式下需要傳入JDBC相應參數,url、user、password和dbtable
2. 保存數據
- write直接保存數據
scala> df.write.
csv jdbc json orc parquet textFile… …
注意
:保存數據的相關參數需寫到上述方法中。如:textFile需傳入加載數據的路徑,jdbc需傳入JDBC相關參數。
2. 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執行存儲操作,SaveMode定義了對數據的處理模式。SaveMode是一個枚舉類,其中的常量包括:
- Append:當保存路徑或者表已存在時,追加內容;
- Overwrite: 當保存路徑或者表已存在時,覆寫內容;
- ErrorIfExists:當保存路徑或者表已存在時,報錯;
- Ignore:當保存路徑或者表已存在時,忽略當前的保存操作。
使用詳解:
df.write.mode(SaveMode.Append).save("… …")
df.write.mode("append").save("… …")
3. 默認數據源Parquet
Parquet是一種流行的列式存儲格式,可以高效的存儲具有嵌套字段的記錄,Parquet格式經常在Hadoop生態圈使用,它也支持SparkSQL的全部數據類型,SparkSQL提供了直接讀取跟存儲Parquet格式文件的方法。並且可以通過format()來指定輸入輸出文件格式。
spark.read.format("csv").load("pwd")
- 加載數據
val df = spark.read.load("examples/src/main/resources/users.parquet")
- 保存數據
df.select("name", " color").write.save("user.parquet")
JSON文件
Spark SQL 能夠自動推測 JSON數據集的結構,並將它加載爲一個Dataset[Row]. 可以通過SparkSession.read.json()去加載一個一個JSON 文件。
目的
:Spark讀寫Json數據,其中數據源可以在本地也可以在HDFS文件系統
注意
:這個JSON文件不是一個傳統的JSON文件,每一行
都得是一個JSON串。格式如下:
{"name":"Michael"}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19}
- 導入隱式轉換
import spark.implicits._
- 加載JSON文件
val path = "examples/src/main/resources/people.json"
val peopleDF = spark.read.json(path)
- 創建臨時表
peopleDF.createOrReplaceTempView("people")
- 數據查詢
val teenagerNamesDF = spark.sql("SELECT name FROM people WHERE age BETWEEN 13 AND 19")
teenagerNamesDF.show()
+------+
| name|
+------+
|Justin|
+------+
MySQL文件
Spark SQL可以通過JDBC從關係型數據庫中讀
取數據的方式創建DataFrame,通過對DataFrame一系列的計算後,還可以將數據再寫
回關係型數據庫中。
目的
:spark讀寫MySQL數據
可在啓動shell時指定相關的數據庫驅動路徑,或者將相關的數據庫驅動放到spark的類路徑下。
cp /opt/mysql-libs/mysql-connector-java-5.1.27-bin.jar /opt/spark/jars
- 啓動spark-shell
bin/spark-shell --master spark://hadoop102:7077 [--jars mysql-connector-java-5.1.27-bin.jar]
- 定義JDBC相關參數配置信息
val connectionProperties = new java.util.Properties()
connectionProperties.put("user", "root")
connectionProperties.put("password", "000000")
- 使用read.jdbc加載數據
val jdbcDF2 = spark.read.jdbc("jdbc:mysql://hadoop102:3306/rdd", "tableName", connectionProperties)
- 使用format形式加載數據
val jdbcDF = spark.read.format("jdbc").option("url", "jdbc:mysql://hadoop102:3306/rdd").option("dbtable", " rddtable").option("user", "root").option("password", "000000").load()
- 使用write.jdbc保存數據
jdbcDF2.write.module(“append”).jdbc("jdbc:mysql://hadoop102:3306/mysql", "db", connectionProperties)
- 使用format形式保存數據
jdbcDF.write
.format("jdbc")
.option("url", "jdbc:mysql://hadoop102:3306/rdd")
.option("dbtable", "rddtable3")
.option("user", "root")
.option("password", "000000")
.save()
其中保存的時候確保主鍵等信息 ,也也可以選擇往mysql中添加數據的module。
Hive
Apache Hive是Hadoop上的SQL引擎,Spark SQL編譯時可以包含Hive支持,也可以不包含。包含Hive支持的Spark SQL可以支持Hive表訪問、UDF(用戶自定義函數)以及Hive查詢語言(HQL)等。spark-shell 默認是Hive支持的;代碼中是默認不支持的,需要手動指定 enableHiveSupport()
。
SparkSQL中的SparkSession 就包含來自Hive跟SparkSQL的數據,這裏的Hive是內置的Hive,跟HBase 裏的內部獨立ZooKeeper類似。工作中要跟外部Hive關聯的。 內部Hive存儲元數據路徑:
/opt/module/spark/metastore_db 來存儲元數據
內嵌Hive 應用
如果要使用內嵌
的Hive,什麼都不用做,直接用就可以了。 前面的 RDD、DF、DS切換的時候數據都是創建的view。isTemporary = true
,但是也可以用內置的Hive來創建table哦!
可以修改其數據倉庫地址,參數爲:–conf spark.sql.warehouse.dir=./wear
注意
:如果你使用的是內部的Hive,在Spark2.0之後,spark.sql.warehouse.dir用於指定數據倉庫的地址,如果你需要是用HDFS作爲路徑,那麼需要將core-site.xml和hdfs-site.xml 加入到Spark conf目錄,否則只會創建master節點上的warehouse目錄,查詢時會出現文件找不到的問題,這是需要使用HDFS,則需要將metastore刪除,重啓集羣。
外部Hive應用
如果想連接外
部已經部署好的Hive,需要通過以下幾個步驟。
將Hive中的hive-site.xml拷貝或者軟連接到Spark安裝目錄下的conf目錄下
。
- 打開
spark shell
,注意帶上訪問Hive元數據庫的JDBC客戶端
bin/spark-shell --master spark://hadoop102:7077 --jars mysql-connector-java-5.1.27-bin.jar
注意
:每次啓動時指定JDBC jar包路徑很麻煩,我們可以選擇將JDBC的驅動包放置在spark的lib目錄下,一勞永逸。
運行Spark SQL CLI
Spark SQL CLI可以很方便的在本地運行Hive元數據服務以及從命令行執行查詢任務。在Spark目錄下執行如下命令啓動Spark SQL CLI,直接執行SQL語句,類似一Hive窗口。
/bin/spark-sql
然後就可以跟在hive的終端一樣進行CRUD即可了,可能會出現 若干bug
代碼中操作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>
源數據:
1,sowhat
2,zhang
3,li
整體思路就是,創建就是跟Hive一樣指定spark hive數據存放spark.sql.warehouse.dir
路徑,指定spark hive的元數據存儲信息metastore_db
。
package com.atguigu.hive
/**
* Created by wuyufei on 05/09/2017.
*/
import java.io.File
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
case class Record(key: Int, value: String)
object HiveOperation {
def main(args: Array[String]) {
Logger.getLogger("org").setLevel(Level.OFF)
Logger.getLogger("akka").setLevel(Level.OFF)
val warehouseLocation = new File("spark-warehouse").getAbsolutePath // 設定數據路徑
println(warehouseLocation)
val spark = SparkSession
.builder()
.appName("Spark Hive Example")
.config("spark.sql.warehouse.dir", warehouseLocation)
.enableHiveSupport() // 注意添加
.master("local[*]")
.config("spark.testing.memory", "471859200")
.getOrCreate()
//import spark.implicits._
spark.sql("CREATE TABLE IF NOT EXISTS user (key INT, value STRING) row format delimited fields terminated by ',' ")
spark.sql("LOAD DATA LOCAL INPATH 'D:/json/kg.txt' INTO TABLE user")
// Queries are expressed in HiveQL
val df = spark.sql("SELECT * FROM user")
df.show()
df.write.format("json").save("D:/json/ssss.json")
spark.stop()
}
}
輸出數據格式:
SparkSQL跟Hive實戰
各種依賴:
<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
<!--<scope>provided</scope>-->
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>${spark.version}</version>
<!--<scope>provided</scope>-->
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>${spark.version}</version>
<!--<scope>provided</scope>-->
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-hive_2.11</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>
</dependencies>
實例代碼
val spark = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
valsc
: SparkContext = spark.sparkContext
package com.atguigu.spark
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, Dataset, SaveMode, SparkSession}
// 訂單號,交易位置,交易日期
case class tbStock(ordernumber: String, locationid: String, dateid: String) extends Serializable
// 訂單號,行號,貨品,數量,單價,銷售額
case class tbStockDetail(ordernumber: String, rownum: Int, itemid: String, number: Int, price: Double, amount: Double) extends Serializable
// 日期,年月,年,月,日,周幾,第幾周,季度,旬,半月
case class tbDate(dateid: String, years: Int, theyear: Int, month: Int, day: Int, weekday: Int, week: Int, quarter: Int, period: Int, halfmonth: Int) extends Serializable
object Practice {
// 將DataFrame插入到Hive表
private def insertHive(spark: SparkSession, tableName: String, dataDF: DataFrame): Unit = {
spark.sql("DROP TABLE IF EXISTS " + tableName)
dataDF.write.saveAsTable(tableName)
}
// 結果寫入到MySQL
private def insertMySQL(tableName: String, dataDF: DataFrame): Unit = {
dataDF.write
.format("jdbc")
.option("url", "jdbc:mysql://localhost:3306/sparksql")
.option("dbtable", tableName)
.option("user", "root")
.option("password", "root")
.mode(SaveMode.Overwrite)
.save()
}
def main(args: Array[String]): Unit = {
// 創建Spark配置
val sparkConf = new SparkConf().setAppName("MockData").setMaster("local[*]")
// 創建Spark SQL 客戶端
val spark = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
import spark.implicits._
// 加載數據到Hive,讀取本地數據 直接 根據結構跟對象 生成DS
val tbStockRdd: RDD[String] = spark.sparkContext.textFile("D:/json/tbStock.txt")
val tbStockDS: Dataset[tbStock] = tbStockRdd.map(_.split(",")).map(attr => tbStock(attr(0), attr(1), attr(2))).toDS
insertHive(spark, "tbStock", tbStockDS.toDF)
val tbStockDetailRdd: RDD[String] = spark.sparkContext.textFile("D:/json/tbStockDetail.txt")
val tbStockDetailDS: Dataset[tbStockDetail] = tbStockDetailRdd.map(_.split(",")).map(attr => tbStockDetail(attr(0), attr(1).trim().toInt, attr(2), attr(3).trim().toInt, attr(4).trim().toDouble, attr(5).trim().toDouble)).toDS
insertHive(spark, "tbStockDetail", tbStockDetailDS.toDF)
val tbDateRdd: RDD[String] = spark.sparkContext.textFile("D:/json/tbDate.txt")
val tbDateDS: Dataset[tbDate] = tbDateRdd.map(_.split(",")).map(attr => tbDate(attr(0), attr(1).trim().toInt, attr(2).trim().toInt, attr(3).trim().toInt, attr(4).trim().toInt, attr(5).trim().toInt, attr(6).trim().toInt, attr(7).trim().toInt, attr(8).trim().toInt, attr(9).trim().toInt)).toDS
insertHive(spark, "tbDate", tbDateDS.toDF)
//需求一: 統計所有訂單中每年的銷售單數、銷售總額
val result1: DataFrame = spark.sql("SELECT c.theyear, COUNT(DISTINCT a.ordernumber), SUM(b.amount) FROM tbStock a JOIN tbStockDetail b ON a.ordernumber = b.ordernumber JOIN tbDate c ON a.dateid = c.dateid GROUP BY c.theyear ORDER BY c.theyear")
insertMySQL("xq1", result1)
//需求二: 統計每年最大金額訂單的銷售額
val result2: DataFrame = spark.sql("SELECT theyear, MAX(c.SumOfAmount) AS SumOfAmount FROM (SELECT a.dateid, a.ordernumber, SUM(b.amount) AS SumOfAmount FROM tbStock a JOIN tbStockDetail b ON a.ordernumber = b.ordernumber GROUP BY a.dateid, a.ordernumber ) c JOIN tbDate d ON c.dateid = d.dateid GROUP BY theyear ORDER BY theyear DESC")
insertMySQL("xq2", result2)
//需求三: 統計每年最暢銷貨品
val result3: DataFrame = spark.sql("SELECT DISTINCT e.theyear, e.itemid, f.maxofamount FROM (SELECT c.theyear, b.itemid, SUM(b.amount) AS sumofamount FROM tbStock a JOIN tbStockDetail b ON a.ordernumber = b.ordernumber JOIN tbDate c ON a.dateid = c.dateid GROUP BY c.theyear, b.itemid ) e JOIN (SELECT d.theyear, MAX(d.sumofamount) AS maxofamount FROM (SELECT c.theyear, b.itemid, SUM(b.amount) AS sumofamount FROM tbStock a JOIN tbStockDetail b ON a.ordernumber = b.ordernumber JOIN tbDate c ON a.dateid = c.dateid GROUP BY c.theyear, b.itemid ) d GROUP BY d.theyear ) f ON e.theyear = f.theyear AND e.sumofamount = f.maxofamount ORDER BY e.theyear")
insertMySQL("xq3", result3)
spark.stop()
}
}
總結
- 學習跟理解RDD、DataFrame、DataSet三者之間的關係,跟如何相互轉換。
- SparkSession操作Json、MySQL、Hive。主要是環境的搭建跟table的操作各種。