【一】簡介 (本文部分圖片來自網絡,侵刪)
Spark SQL是Spark用來處理結構化數據的一個模塊,它提供了一個編程抽象叫做DataFrame並且作爲分佈式SQL查詢引擎的作用
【二】特點
【2.1】易整合
Spark SQL可以使用SQL或熟悉的DataFrame API在Spark程序中查詢結構化數據,可在Java,Scala,Python和R中使用
【2.2】統一的數據訪問方式
DataFrame和SQL提供了一種訪問各種數據源的通用方法,包括Hive,Avro,Parquet,ORC,JSON和JDBC。甚至可以跨這些源聯接數據
【2.3】兼容Hive
Spark SQL支持HiveQL語法以及Hive SerDes和UDF,從而訪問現有的Hive倉庫
【2.4】標準的數據連接
服務器模式爲商業智能工具提供了行業標準的JDBC和ODBC連接
【三】概述
【3.1】SparkSQL可以看做是一個轉換層,向下對接各種不同的結構化數據源,向上提供不同的數據訪問方式
【3.2】
在SparkSQL中Spark爲我們提供了兩個新的抽象,分別是DataFrame和DataSet;他們和RDD有什麼區別呢?首先從版本的產生上來看:RDD (Spark1.0) —> Dataframe(Spark1.3) —> Dataset(Spark1.6)
如果同樣的數據都給到這三個數據結構,他們分別計算之後,都會給出相同的結果。不同是的他們的執行效率和執行方式
注意:在後期的Spark版本中,DataSet會逐步取代RDD和DataFrame成爲唯一的API接口
【3.3】RDD
RDD概述
- 【1】RDD意爲彈性分佈式數據集,是一個懶執行的、不可變的可以支持Lambda表達式的並行數據集合
- 【2】RDD的最大好處就是簡單,API的人性化程度很高
- 【3】RDD的劣勢是性能限制,它是一個JVM駐內存對象,這也就決定了存在GC的限制和數據增加時Java序列化成本的升高
【3.4】DataFrame
DataFrame概述
- 【1】與RDD類似,DataFrame也是一個分佈式數據數據集。然而DataFrame更像傳統數據庫的二維表格,除了數據以外,還記錄數據的結構信息,即Schema信息
- 【2】與Hive類似,DataFrame也支持嵌套數據類型(struct、array和map)
- 【3】從API易用性的角度上看,DataFrame API提供的是一套高層的關係操作,比函數式的RDD API要更加友好,門檻更低。由於與R和Pandas的DataFrame類似,Spark DataFrame很好地繼承了傳統單機數據分析的開發體驗
- 【1】上圖直觀地體現了DataFrame和RDD的區別。左側的RDD[Person]雖然以Person爲類型參數,但Spark框架本身不瞭解Person類的內部結構。而右側的DataFrame卻提供了詳細的結構信息,使得Spark SQL可以清楚地知道該數據集中包含哪些列,每列的名稱和類型各是什麼。DataFrame多了數據的結構信息,即Schema。RDD是分佈式的Java對象的集合,DataFrame是分佈式的Row對象的集合。DataFrame除了提供了比RDD更豐富的算子以外,更重要的特點是提升
執行效率
、減少數據讀取
以及執行計劃的優化
,比如filter下推、裁剪等… - 【2】DataFrame是爲數據提供了Schema的視圖,可以把它當做數據庫中的一張表來對待
- 【3】DataFrame也是
懶執行
的,性能上比RDD要高,主要有兩方面原因:
①.定製化內存管理
②.數據以二進制的方式存在於非堆內存,節省了大量空間之外,還擺脫了GC的限制
- 【1】優化的執行計劃 ,查詢計劃通過Spark catalyst optimiser進行優化
- 【2】比如下面一個例子
- 【3】爲了說明查詢優化,我們來看上圖展示的人口數據分析的示例。圖中構造了兩個DataFrame,將它們join之後又做了一次filter操作。如果原封不動地執行這個執行計劃,最終的執行效率是不高的。因爲join是一個代價較大的操作,也可能會產生一個較大的數據集
如果我們能將filter下推到 join下方,先對DataFrame進行過濾,再join過濾後的較小的結果集,便可以有效縮短執行時間。而Spark SQL的查詢優化器正是這樣做的。簡而言之,邏輯查詢計劃優化就是一個利用基於關係代數的等價變換,將高成本的操作替換爲低成本操作的過程
得到的優化執行計劃在轉換成物理執行計劃的過程中,還可以根據具體的數據源的特性將過濾條件下推至數據源內。最右側的物理執行計劃中Filter之所以消失不見,就是因爲溶入了用於執行最終的讀取操作的表掃描節點內
對於普通開發者而言,查詢優化器的意義在於,即便是經驗並不豐富的程序員寫出的次優的查詢,也可以被儘量轉換爲高效的形式予以執行 - 【6】DataFrame的
劣勢
在於在編譯期缺少類型安全檢查
,導致運行時出錯
【3.5】DataSet
DataSet概述
- 【1】DataSet是Dataframe API的一個擴展,是Spark最新的數據抽象
- 【2】DataSet用戶友好的API風格,既具有
類型安全檢查
也具有DataFrame的查詢優化特性
- 【3】DataSet支持
編解碼器
,當需要訪問非堆
上的數據時可以避免反序列化整個對象
,提高了效率 - 【4】樣例類被用來在DataSet中定義數據的結構信息,樣例類中每個屬性的名稱直接映射到DataSet中的字段名稱
- 【5】DataFrame和DataSet之間可以互相轉換,DataFrame/DataSet[Row] + 類型 =DataSet [類型] ,所以可以通過
as
方法將DataFrame轉換爲DataSet,也可以通過toDF
方法將DataSet轉換爲DataFrame - 【6】DataSet是強類型的,比如可以有DataSet[Car],DataSet[Person]…
DataFrame只是知道字段,但是不知道字段的類型,所以在執行這些操作的時候是沒辦法在編譯的時候檢查是否類型失敗的,比如你可以對一個String進行減法操作,在執行的時候才報錯,而DataSet不僅僅知道字段,而且知道字段類型,所以有更嚴格的錯誤檢查。就跟JSON對象和類對象之間的類比,如下圖:
注意:RDD讓我們能夠決定怎麼做,而DataFrame和DataSet讓我們決定做什麼,控制的粒度不一樣
【3.6】RDD、DataFrame和DataSet三者的共性
- 【1】RDD、DataFrame、DataSet全都是Spark平臺下的分佈式彈性數據集,爲處理超大型數據提供便利
- 【2】三者都有惰性機制,在進行創建、轉換,如map方法時,不會立即執行,只有在遇到Action如foreach時,三者纔會開始遍歷運算,極端情況下,如果代碼裏面有創建、轉換,但是後面沒有在Action中使用對應的結果,在執行時會被直接跳過
val sparkconf = new SparkConf().setMaster("local[*]").setAppName("sparkSqlDemo")
val spark = SparkSession.builder().config(sparkconf).getOrCreate()
val rdd = spark.sparkContext.parallelize(Seq(("a", 1), ("b", 1), ("a", 1)))
// 下面這個map並不會運行
rdd.map{line=>
println("運行到這裏啦...")
line._1
}
- 【3】三者都會根據Spark的內存情況自動緩存運算,這樣即使數據量很大,也不用擔心會內存溢出
- 【4】三者都有partition的概念
- 【5】三者有許多共同的函數,如filter,排序等…
- 【6】在對DataFrame和DataSet進行操作許多操作都需要導入下方隱式函數的支持
注意:spark不是包名,是SparkSession的名字
import spark.implicits._
- 【7】DataFrame和DataSet均可使用模式匹配獲取各個字段的值和類型
DataFrame:
df.map {
case Row(id: Int, name: String, age: Int) => {
println(s"${id},${name},${age}")
name
}
case _ => ""
}
DataSet:
ds.map {
case User(id: Int, name: String, age: Int) => {
println(s"${id},${name},${age}")
name
}
case _ => ""
}
【3.7】RDD、DataFrame和DataSet三者的區別
【1】RDD:
RDD可以設置類型參數,但RDD並不瞭解其內部結構
【2】DataFrame:
1.與RDD和DataSet不同,DataFrame每一行的類型固定爲Row,只有通過解析才能獲取各個字段的值,如下:👇
df.foreach{
line => {
val id = line.getAs[Int]("id")
val name = line.getAs[String]("name")
val age = line.getAs[Int]("age")
println(s"${id} - ${name} - ${age}")
val id2 = line.getAs[Int](0)
val name2 = line.getAs[String](1)
val age2 = line.getAs[Int](2)
println(s"${id2} -- ${name2} -- ${age2}")
}
}
2.DataFrame與DataSet一般與 Spark ml 同時使用
3.DataFrame與DataSet均支持 SparkSql 的操作,比如select,groupby之類,
還能註冊臨時表/視窗,進行sql語句操作,如下:👇
df.createOrReplaceTempView("user")
spark.sql("select id,name,age from user").show()
4.DataFrame 與 DataSet 支持一些特別方便的保存方式,默認讀取和輸出的格式都是 parquet 格式
也可以保存成json、csv等格式... 如下:👇
【讀取】
1.讀取數據通用方式1:讀取json格式
val df:DataFrame = spark.read.json("input/user.json")
或 val df:DataFrame = spark.read.format("json").load("input/user.json")
2.讀取數據默認方式2:讀取parquet格式
val df:DataFrame = spark.read.load("input/user.parquet")
【保存】
1.保存數據通用方式1:保存爲指定格式,例如:json
df.write.format("csv").mode(SaveMode.Overwrite).save("outputcsv")
df.write.format("json").mode("append").save("outputjson")
2.保存數據默認方式2:保存爲默認parquet格式,如果文件夾存在則覆蓋
df.write.mode(SaveMode.Overwrite).save("outputparquet")
【3】DataSet:
1.DataSet 和 DataFrame 擁有完全相同的成員函數,區別只是每一行的數據類型不同
2.DataFrame 也可以叫 DataSet[Row],每一行的類型是Row,不解析,每一行究竟有哪些字段,各個字段又是什麼類型都無從得知,
只能用上面提到的 getAS 方法或者三者共性中的第七條提到的模式匹配拿出特定字段
3.在DataSet中,每一行是什麼類型是不一定的,在自定義了case class樣例類之後可以很自由的獲得每一行的信息
4.可以看出,DataSet在需要訪問列中的某個字段時是非常方便的,然而,如果要寫一些適配性很強的函數時,
如果使用DataSet,行的類型又不確定,可能是各種case class,無法實現適配,這時候用DataFrame即DataSet[Row]就能比較好的解決問題
【3.8】三者的相互轉換
【1】 RDD + 結構 = DataFrame
方法:rdd.toDF(字段…)
【2】DataFrame + 類型 = DataSet
方法:df.as[類型]
【3】RDD + 結構 + 類型 = DataSet (RDD + 類(樣例類) = DataSet)
方法:rdd.toDS
【4】DataSet - 類型 = DataFrame
方法:ds.toDF
【5】DataFrame - 結構 = RDD
方法:df.rdd
【四】SparkSQL解析
【1】新的起始點SparkSession
- 在老的版本中,SparkSQL提供兩種SQL查詢起始點,一個叫SQLContext,用於Spark自己提供的SQL查詢,一個叫HiveContext,用於連接Hive的查詢,SparkSession是Spark最新的SQL查詢起始點,實質上是SQLContext和HiveContext的組合,所以在SQLContext和HiveContext上可用的API在SparkSession上同樣是可以使用的。SparkSession內部封裝了SparkContext,所以計算實際上是由SparkContext完成的
- SparkSession.builder 用於創建一個SparkSession
import spark.implicits._的引入是用於將DataFrames隱式轉換成RDD,使DataFream、DataSet能夠使用RDD中的方法
【2】IDEA創建SparkSQL程序
1.IDEA中程序的打包和運行方式都和SparkCore類似,Maven依賴中需要添加新的依賴項:
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>${spark.version}</version>
<scope>provided</scope>
</dependency>
2.HelloWorld :
object HelloWorld {
def main(args: Array[String]) {
//創建SparkSession
val spark = SparkSession
.builder()
.master("local[*]")
.appName("sparkDemo")
.config(new SparkConf())
.getOrCreate()
// For implicit conversions like converting RDDs to DataFrames
import spark.implicits._
val df = spark.read.json("input/user.json")
// Displays the content of the DataFrame to stdout
df.show()
df.filter($"age" > 21).show()
df.createOrReplaceTempView("user")
spark.sql("SELECT * FROM user where age > 21").show()
spark.stop()
}
}
3.創建DataFrame:
- 在Spark SQL中SparkSession是創建DataFrames和執行SQL的入口,創建DataFrames有三種方式,一種是可以從一個存在的RDD進行轉換,還可以從Hive Table進行查詢返回,或者通過Spark的數據源進行創建
1、從Spark數據源進行創建:
val df = spark.read.json("examples/src/main/resources/people.json")
2、從RDD進行轉換:
val peopleRdd = sc.textFile("examples/src/main/resources/people.txt")
val peopleDF = peopleRdd.map(_.split(",")).map(paras => (paras(0),paras(1).trim().toInt)).toDF("name","age")
4.DataFrame常用操作:
- 【1】DSL風格語法
val df = spark.read.json("examples/src/main/resources/people.json")
df.show()
import spark.implicits._
df.printSchema()
df.select("name").show()
df.select($"name", $"age" + 1).show()
df.filter($"age" > 21).show()
df.groupBy("age").count().show()
- 【2】SQL風格語法
df.createOrReplaceTempView("people")
val sqlDF = spark.sql("SELECT * FROM people")
sqlDF.show()
df.createGlobalTempView("people")
spark.sql("SELECT * FROM global_temp.people").show()
spark.newSession().sql("SELECT * FROM global_temp.people").show()
注意:臨時表是Session範圍內的,Session退出後,表就失效了。
如果想應用範圍內有效,可以使用全局表。注意使用全局表時需要全路徑訪問,如:global_temp.people
5.DataSet:
Dataset是具有強類型的數據集合,需要提供對應的類型信息
import spark.implicits._
case class Person(name: String, age: Long)
val caseClassDS = Seq(Person("Andy", 32)).toDS()
caseClassDS.show()
val primitiveDS = Seq(1, 2, 3).toDS()
primitiveDS.map(_ + 1).collect() // Returns: Array(2, 3, 4)
val path = "examples/src/main/resources/people.json"
val peopleDS = spark.read.json(path).as[Person]
peopleDS.show()
6.DataSet和RDD互操作
Spark SQL支持通過兩種方式將存在的RDD轉換爲DataSet,轉換的過程中需要讓DataSet獲取RDD中的Schema信息,主要有兩種方式,一種是通過反射來獲取RDD中的Schema信息。這種方式適合於列名已知的情況下。第二種是通過編程接口的方式將Schema信息應用於RDD,這種方式可以處理那種在運行時才能知道列的方式
①.通過反射獲取Scheam
SparkSQL能夠自動將包含有case類的RDD轉換成DataSet,case類定義了table的結構,case類屬性通過反射變成了表的列名。case類可以包含諸如Seqs或者Array等複雜的結構
import spark.implicits._
val peopleDF = spark.sparkContext
.textFile("examples/src/main/resources/people.txt")
.map(_.split(","))
.map(attributes => Person(attributes(0), attributes(1).trim.toInt))
.toDF()
peopleDF.createOrReplaceTempView("people")
val teenagersDF = spark.sql("SELECT name, age FROM people WHERE age BETWEEN 13 AND 19")
teenagersDF.map(teenager => "Name: " + teenager(0)).show()
teenagersDF.map(teenager => "Name: " + teenager.getAs[String]("name")).show()
implicit val mapEncoder = org.apache.spark.sql.Encoders.kryo[Map[String, Any]]
teenagersDF.map(teenager => teenager.getValuesMap[Any](List("name", "age"))).collect()
②.通過編程設置Schema(StructType)
如果case類不能夠提前定義,可以通過下面三個步驟定義一個DataFrame
【1】、創建一個多行結構的RDD;
【2】、創建用StructType來表示的行結構信息
【3】、通過SparkSession提供的createDataFrame方法來應用Schema
import spark.implicits._
import org.apache.spark.sql.types._
val peopleRDD = spark.sparkContext.textFile("examples/src/main/resources/people.txt")
val schema = StructType(Seq(
StructField("name",StringType,true),
StructField("age",StringType,true)
))
import org.apache.spark.sql._
val rowRDD = peopleRDD
.map(_.split(","))
.map(attributes => Row(attributes(0), attributes(1).trim))
val peopleDF = spark.createDataFrame(rowRDD, schema)
peopleDF.createOrReplaceTempView("people")
val results = spark.sql("SELECT name FROM people")
results.map(attributes => "Name: " + attributes().show()
7.用戶自定義函數
通過spark.udf功能用戶可以自定義函數
①.用戶自定義UDF函數
val df = spark.read.json("examples/src/main/resources/people.json")
df.show()
spark.udf.register("addName", (x:String)=> "Name:"+x)
df.createOrReplaceTempView("people")
spark.sql("Select addName(name), age from people").show()
②.用戶自定義聚合函數
強類型的Dataset和弱類型的DataFrame都提供了相關的聚合函數, 如 count(),countDistinct(),avg(),max(),min()。除此之外,用戶可以設定自己的自定義聚合函數👇
弱類型:
object SparkSQL_UDAF1 {
def main(args: Array[String]): Unit = {
//創建SparkSQL環境對象
val spark = SparkSession
.builder()
.master("local[*]")
.appName("SparkSQL05_UDAF")
.config(new SparkConf())
.getOrCreate()
import spark.implicits._
//自定義聚合函數
val udaf = new MyAgeAvgFunction
//註冊聚合函數
spark.udf.register("avgAge",udaf)
val df: DataFrame = spark.read.json("input/user.json")
df.createOrReplaceTempView("user")
spark.sql("select avgAge(age) from user").show()
//釋放資源
spark.stop()
}
//自定義聚合函數(弱類型)
//1.繼承UserDefinedAggregateFunction
//2.實現方法
class MyAgeAvgFunction extends UserDefinedAggregateFunction{
//函數輸入的數據結構
override def inputSchema: StructType = new StructType().add("age",LongType)
//計算時的數據結構
override def bufferSchema: StructType = new StructType().add("sum",LongType).add("count",LongType)
//函數返回的數據類型
override def dataType: DataType = DoubleType
//函數是否穩定
override def deterministic: Boolean = true
//計算之前,緩衝區的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
//sum
buffer(0) = 0L
//count
buffer(1) = 0L
}
//根據查詢結果更新緩衝區數據
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
//sum
buffer(0) = buffer.getLong(0) + input.getLong(0)
//count
buffer(1) = buffer.getLong(1) + 1
}
//將多個節點的緩衝區合併
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
//sum
buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0)
//count
buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)
}
//計算最終結果
override def evaluate(buffer: Row): Any = {
buffer.getLong(0).toDouble / buffer.getLong(1)
}
}
}
強類型:
object SparkSQL_UDAF2 {
def main(args: Array[String]): Unit = {
//創建SparkSQL環境對象
val spark = SparkSession
.builder()
.master("local[*]")
.appName("SparkSQL06_UDAF")
.config(new SparkConf())
.getOrCreate()
import spark.implicits._
//自定義聚合函數
val udaf = new MyAgeAvgFunction
//將聚合函數轉換爲查詢列
val avgCol: TypedColumn[UserBean, Double] = udaf.toColumn.name("avgAge")
val df: DataFrame = spark.read.json("input/user.json")
val ds = df.as[UserBean]
//應用函數
ds.select(avgCol).show()
//釋放資源
spark.stop()
}
case class UserBean(name: String, age: BigInt)
case class AvgBuffer(var sum: BigInt, var count: Int)
//自定義聚合函數(強類型)
//1.繼承Aggregator,設置泛型
//2.實現方法
class MyAgeAvgFunction extends Aggregator[UserBean, AvgBuffer, Double] {
//初始化
override def zero: AvgBuffer = AvgBuffer(0, 0)
//聚合數據
override def reduce(b: AvgBuffer, a: UserBean): AvgBuffer = {
b.sum += a.age
b.count += 1
b
}
//緩衝區合併操作
override def merge(b1: AvgBuffer, b2: AvgBuffer): AvgBuffer = {
b1.sum += b2.sum
b1.count += b2.count
b1
}
//完成最終計算
override def finish(reduction: AvgBuffer): Double = {
reduction.sum.toDouble / reduction.count
}
override def bufferEncoder: Encoder[AvgBuffer] = Encoders.product
override def outputEncoder: Encoder[Double] = Encoders.scalaDouble
}
}