第三天:SparkSQL

第1章 Spark SQL概述

什麼是Spark SQL

Spark SQL是Spark用來處理結構化數據的一個模塊,它提供了2個編程抽象:DataFrameDataSet,來作爲分佈式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的特點

  1. 易整合
    在這裏插入圖片描述
  2. 統一的數據訪問方式
    在這裏插入圖片描述
  3. 兼容Hive
    在這裏插入圖片描述
  4. 標準的數據連接
    在這裏插入圖片描述

什麼是DataFrame

在Spark中,DataFrame是一種以RDD爲基礎的分佈式數據集,類似於傳統數據庫中的二維表格DataFrameRDD的主要區別在於,前者帶有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 SQL優化執行引擎的優點。DataSet也可以使用功能性的轉換(操作map,flatMap,filter等等)。

  1. 是DataFrame API的一個擴展,是SparkSQL最新的數據抽象;
  2. 用戶友好的API風格,既具有類型安全檢查也具有DataFrame的查詢優化特性;
  3. 用樣例類來對DataSet中定義數據的結構信息,樣例類中每個屬性的名稱直接映射到DataSet中的字段名稱;
  4. 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的組合,所以在SQLContextHiveContext上可用的API在SparkSession上同樣是可以使用的。SparkSession內部封裝了sparkContext,所以計算實際上是由sparkContext完成的
在這裏插入圖片描述

2. DataFrame

創建
在Spark SQL中SparkSession是創建DataFrame和執行SQL的入口,創建DataFrame有三種方式:通過Spark的數據源進行創建從一個存在的RDD進行轉換;還可以從Hive Table進行查詢返回。

從Spark數據源進行創建
  1. 查看Spark數據源進行創建的文件格式
scala> spark.read.
csv   format   jdbc   json   load   option   options   orc   parquet   schema   table   text   textFile
  1. 讀取json文件創建DataFrame
scala> val df = spark.read.json("/opt//people.json") //讀取分區上的文件,本地用 file:///pwd
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
  1. 展示結果
scala> df.show
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+
SQL風格語法(主要)
  1. 創建一個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]
  1. 對DataFrame創建一個臨時表,View是隻讀的,Table有改的意思哦。
scala> df.createOrReplaceTempView("people")
  1. 通過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 = []
  1. 結果展示
scala> sqlDF.show
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+

注意:普通臨時表是Session範圍內的,如果想應用範圍內有效,可以使用全局臨時表。使用全局臨時表時需要全路徑訪問,如:global_temp.people
5. 對於DataFrame創建一個全局表

scala> df.createGlobalTempView("people")
  1. 通過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風格語法(次要)
  1. 創建一個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]
  1. 查看DataFrame的Schema信息
scala> df.printSchema
root
 |-- age: long (nullable = true)
 |-- name: string (nullable = true)
  1. 只查看”name”列數據
scala> df.select("name").show()
+-------+
|   name|
+-------+
|Michael|
|   Andy|
| Justin|
+-------+
  1. 查看”name”列數據以及”age+1”數據
scala> df.select($"name", $"age" + 1).show()
+-------+---------+
|   name|(age + 1)|
+-------+---------+
|Michael|     null|
|   Andy|       31|
| Justin|       20|
+-------+---------+
  1. 查看”age”大於”21”的數據
scala> df.filter($"age" > 21).show()
+---+----+
|age|name|
+---+----+
| 30|Andy|
+---+----+
  1. 按照”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
  1. 通過手動確定轉換
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. 通過反射確定(需要用到樣例類)
  1. 創建一個樣例類
scala> case class People(name:String, age:Int)
  1. 根據樣例類將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. 通過編程的方式(瞭解)
  1. 導入所需的類型
scala> import org.apache.spark.sql.types._
import org.apache.spark.sql.types._
  1. 創建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))
  1. 導入所需的類型
scala> import org.apache.spark.sql.Row
import org.apache.spark.sql.Row
  1. 根據給定的類型創建二元組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
  1. 根據數據及給定的schema創建DataFrame
scala> val dataFrame = spark.createDataFrame(data, structType)
dataFrame: org.apache.spark.sql.DataFrame = [name: string, age: int]
DataFrame轉換爲RDD

直接調用rdd即可

  1. 創建一個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]
  1. 將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 關心的是行,所以轉換的時候是按照行來轉換的
  1. 打印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是具有強類型的數據集合,需要提供對應的類型信息。

創建
  1. 創建一個樣例類
scala> case class Person(name: String, age: Long)
defined class Person
  1. 創建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等複雜的結構。

  1. 創建一個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
  1. 創建一個樣例類
scala> case class Person(name: String, age: Long)
defined class Person
  1. 將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方法即可。

  1. 創建一個DataSet
scala> val DS = Seq(Person("Andy", 32)).toDS()
DS: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
  1. 將DataSet轉換爲RDD
scala> DS.rdd
res11: org.apache.spark.rdd.RDD[Person] = MapPartitionsRDD[15] at rdd at <console>:28

4. DataFrame與DataSet的互操作

DataFrame轉DataSet
  1. 創建一個DateFrame
scala> val df = spark.read.json("examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
  1. 創建一個樣例類
scala> case class Person(name: String, age: Long)
defined class Person
  1. 將DataFrame轉化爲DataSet,添加類型
scala> df.as[Person]
res14: org.apache.spark.sql.Dataset[Person] = [age: bigint, name: string]
Dataset轉DataFrame
  1. 創建一個樣例類
scala> case class Person(name: String, age: Long)
defined class Person
  1. 創建DataSet
scala> val ds = Seq(Person("Andy", 32)).toDS()
ds: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]
  1. 將DataSet轉化爲DataFrame
scala> val df = ds.toDF
df: org.apache.spark.sql.DataFrame = [name: string, age: bigint]
  1. 展示
scala> df.show
+----+---+
|name|age|
+----+---+
|Andy| 32|
+----+---+

這種方法就是在給出每一列的類型後,使用as方法,轉成Dataset,這在數據類型是DataFrame又需要針對各個字段處理時極爲方便。在使用一些特殊的操作時,一定要加上import spark.implicits._不然toDFtoDS無法使用。

RDD、DataFrame、DataSet

在這裏插入圖片描述
在SparkSQL中Spark爲我們提供了兩個新的抽象,DataFrame跟DataSet,他們跟RDD的區別首先從版本上來看

RDD(Spark1.0) ----> DataFrame(Spark1.3)---->DataSet(Spark1.6)

如果同樣的數據都給到了這三個數據結構,他們分別計算後會得到相同的結果,不同的是他們的執行效率跟執行方式,在後期的Spark版本中DataSet會逐步取代另外兩者稱爲唯一接口。

所以在做一個整體的項目時候,一般還是以Java爲主,只有在涉及到迭代式計算採用到Scala這樣到函數式編程。

相同點
  1. RDD、DataFrame、DataSet全部都是平臺下到分佈式彈性數據集,爲處理超大型數據提供了便利
  2. 三者都有惰性機制,在創建,轉換,如map方法時候不會立即執行,只有遇到了Action算子比如foreach,三者纔會開始遍歷數據
  3. 三者都會根據spark的內存進行自動緩存運算,當數據量超大時候會自動寫到磁盤,不用擔心內存溢出。
  4. 三者都有partition的概念。
  5. 三者都有許多共同函數,如filter,排序等。
  6. 在對DataFrame跟DataSet進行許多操作都要import spark.implicits._
  7. 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:
  1. RDD 一般跟sparkMlib 同時使用
  2. RDD 不支持sparkSQL操作
  1. DataFrame
  1. 跟RDD和DataSet不同,DataFrame 每一行類型都固定爲Row,每一列值無法直接訪問,只有通過解析纔可以獲得各個字段。
testDf.foreach{
	line=>
	val col1 = line.getAs[String]("col1")
	val col2 = line.getAs[String]("col2")
}
  1. DataFrame跟DataSet一般不跟sparkMlib共同使用。
  2. 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)
  1. 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()
  1. DataSet
  1. DataSet 跟DataFrame擁有完全一樣的成員函數,唯一區別就是每一行數據類型不同。
  2. 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
  1. 創建DataFrame
scala> val df = spark.read.json("examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
  1. 打印數據
scala> df.show()
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+
  1. 註冊UDF,功能爲在數據前添加字符串
scala> spark.udf.register("addName", (x:String)=> "Name:"+x)
res5: org.apache.spark.sql.expressions.UserDefinedFunction = UserDefinedFunction(<function1>,StringType,Some(List(StringType)))
  1. 創建臨時表
scala> df.createOrReplaceTempView("people")
  1. 應用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. 加載數據
  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. 保存數據
  1. write直接保存數據
scala> df.write.
csv  jdbc   json  orc   parquet textFile… …

注意:保存數據的相關參數需寫到上述方法中。如:textFile需傳入加載數據的路徑,jdbc需傳入JDBC相關參數。
2. format指定保存數據類型

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

用法詳解

  1. format("…"):指定保存的數據類型,包括"csv"、“jdbc”、“json”、“orc”、“parquet"和"textFile”。
  2. save ("…"):在"csv"、“orc”、"parquet"和"textFile"格式下需要傳入保存數據的路徑。
  3. option("…"):在"jdbc"格式下需要傳入JDBC相應參數,url、user、password和dbtable
  4. 文件保存選項
    可以採用SaveMode執行存儲操作,SaveMode定義了對數據的處理模式。SaveMode是一個枚舉類,其中的常量包括:
  1. Append:當保存路徑或者表已存在時,追加內容;
  2. Overwrite: 當保存路徑或者表已存在時,覆寫內容;
  3. ErrorIfExists:當保存路徑或者表已存在時,報錯;
  4. 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")
  1. 加載數據
val df = spark.read.load("examples/src/main/resources/users.parquet") 
  1. 保存數據
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}
  1. 導入隱式轉換
import spark.implicits._
  1. 加載JSON文件
val path = "examples/src/main/resources/people.json"
val peopleDF = spark.read.json(path)
  1. 創建臨時表
peopleDF.createOrReplaceTempView("people")
  1. 數據查詢
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

  1. 啓動spark-shell
bin/spark-shell --master spark://hadoop102:7077 [--jars mysql-connector-java-5.1.27-bin.jar]
  1. 定義JDBC相關參數配置信息
val connectionProperties = new java.util.Properties()
connectionProperties.put("user", "root")
connectionProperties.put("password", "000000")
  1. 使用read.jdbc加載數據
val jdbcDF2 = spark.read.jdbc("jdbc:mysql://hadoop102:3306/rdd", "tableName", connectionProperties)
  1. 使用format形式加載數據
val jdbcDF = spark.read.format("jdbc").option("url", "jdbc:mysql://hadoop102:3306/rdd").option("dbtable", " rddtable").option("user", "root").option("password", "000000").load()
  1. 使用write.jdbc保存數據
jdbcDF2.write.module(“append”).jdbc("jdbc:mysql://hadoop102:3306/mysql", "db", connectionProperties)
  1. 使用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,需要通過以下幾個步驟。

  1. 將Hive中的hive-site.xml拷貝或者軟連接到Spark安裝目錄下的conf目錄下
    在這裏插入圖片描述
  2. 打開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()
val sc: 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()
  }
}

總結

  1. 學習跟理解RDD、DataFrame、DataSet三者之間的關係,跟如何相互轉換。
  2. SparkSession操作Json、MySQL、Hive。主要是環境的搭建跟table的操作各種。

參考

Spark全套資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章