Spark學習(六):Spark SQL一

目錄

1 Spark SQL

1.1 Spark SQL是什麼

1.2 Spark的優點

1.3 RDD vs DataFrame vs Dataset

1.3.1 RDD

1.3.2 DataFrame

1.3.3 Dataset

1.3.4 三者的共性

1.3.5 三者的區別

2 Spark SQL編程

2.1 spark-shell編程

2.2 IDEA創建Spark SQL 程序

3 Spark SQL解析

3.1 新的起始點SparkSession

3.2 創建DataFrame

3.3 DataFrame的常用操作

3.3.1 DSL(Domain Specific Language)風格語法

3.3.2 SQL風格語法

3.4 創建Dataset

3.5 Dataset和RDD互操作

3.5.1 通過反射獲取Scheam

3.5.2 通過編程設置schema

3.6 類型之間的轉換總結

3.6.1 DataFrame/Dataset轉RDD:

3.6.2 RDD轉DataFrame:

3.6.3 RDD轉Dataset:

3.6.4 Dataset轉DataFrame:

3.6.5 DataFrame轉Dataset:

3.7 用戶自定義函數

3.7.1 用戶自定義UDF函數

3.7.2 用戶自定義聚合函數


1 Spark SQL

1.1 Spark SQL是什麼

官網:http://spark.apache.org/sql/

Spark SQL是Apache Spark用來處理結構化數據的一個模塊,它提供了一個最核心的編程抽象叫做DataFrame,並且作爲分佈式SQL查詢引擎的作用。

1.2 Spark的優點

  • 易整合,Spark SQL DataFrame API 多種語言都支持
  • 統一的數據訪問形式,都是以read()方法進行訪問
  • 兼容Hive,之前Hive查詢底層是解析成MapReduce運行,現在在hive查詢被解析爲spark程序,執行效率非常快!
  • 標準的數連接,通過JDBC或ODBC連接

SparkSQL可以看做是一個轉換層,向下對接各種不同的結構化數據源,向上提供不同的數據訪問方式。

1.3 RDD vs DataFrame vs Dataset

 

 

Sql

DataFram

Dataset

Syntax errors  

Run time

Compile time

Compile time

Analysis errors

Run time

Run time

Compile time

Dataframe的劣勢在於在編譯期缺少類型安全檢查,導致運行時出錯,而Dataset是一個強數據類型,在編譯時期就會檢查數據類型

在SparkSQL中Spark爲我們提供了兩個新的抽象,分別是DataFrame和DataSet。他們和RDD有什麼區別呢?首先從版本的產生上來看:

RDD (Spark1.0) —> Dataframe(Spark1.3) —> Dataset(Spark1.6測試)—>Dataset(Spark2.x之後)

spark1.6—>2.x改變:本文用的版本是spark2.2.0

  1. 由SparkSession作爲spark程序的入口,取代了之前的SparkContext和HiveContext;保留是爲了向後兼容
  2. 數據集API和Dataframe API是統一的,在Scala中DataFrame成爲類型別名Dataset[Row],相當於DateFrame=Dateset[Row];而Java API中必須替換爲Dateset<Row>
  3. 一些數據集和DataFrame API 的優化

1.3.1 RDD

RDD是一個懶執行的不可變的可以支持Lambda表達式的並行數據集合。

RDD的最大好處就是簡單,API的人性化程度很高。

RDD的劣勢是性能限制,它是一個JVM駐內存對象,這也就決定了存在GC的限制和數據增加時Java序列化成本的升高。

1.3.2 DataFrame

與RDD類似,DataFrame也是一個分佈式數據容器。然而DataFrame更像傳統數據庫的二維表格,除了數據以外,還記錄數據的結構信息,即schema。同時,與Hive類似,DataFrame也支持嵌套數據類型(struct、array和map)。從API易用性的角度上看,DataFrame API提供的是一套高層的關係操作,比函數式的RDD API要更加友好,門檻更低。

左側的RDD[Person]雖然以Person爲類型參數,但Spark框架本身不瞭解Person類的內部結構。而右側的DataFrame卻提供了詳細的結構信息,使得Spark SQL可以清楚地知道該數據集中包含哪些列,每列的名稱和類型各是什麼。DataFrame多了數據的結構信息,即schema。RDD是分佈式的Java對象的集合。DataFrame是分佈式的Row對象的集合。DataFrame除了提供了比RDD更豐富的算子以外,更重要的特點是提升執行效率、減少數據讀取以及執行計劃的優化,比如filter下推、裁剪等。

DataFrame是爲數據提供了Schema的視圖。可以把它當做數據庫中的一張表來對待

DataFrame也是懶執行的。

性能上比RDD要高,主要有兩方面原因: 

  1. 定製化內存管理
  2. 數據以二進制的方式存在於非堆內存,節省了大量空間之外,還擺脫了GC的限制。

 

圖中構造了兩個DataFrame,將它們join之後又做了一次filter操作。如果原封不動地執行這個執行計劃,最終的執行效率是不高的。因爲join是一個代價較大的操作,也可能會產生一個較大的數據集。如果我們能將filter下推到 join下方,先對DataFrame進行過濾,再join過濾後的較小的結果集,便可以有效縮短執行時間。而Spark SQL的查詢優化器正是這樣做的。簡而言之,邏輯查詢計劃優化就是一個利用基於關係代數的等價變換,將高成本的操作替換爲低成本操作的過程。

1.3.3 Dataset

  1. 是Dataframe API的一個擴展,是Spark最新的數據抽象
  2. 用戶友好的API風格,既具有類型安全檢查也具有Dataframe的查詢優化特性。
  3. Dataset支持編解碼器,當需要訪問非堆上的數據時可以避免反序列化整個對象,提高了效率。
  4. 樣例類被用來在Dataset中定義數據的結構信息,樣例類中每個屬性的名稱直接映射到DataSet中的字段名稱。
  5. Dataframe是Dataset的特例,DataFrame=Dataset[Row] ,所以可以通過as方法將Dataframe轉換爲Dataset。Row是一個類型,跟Car、Person這些的類型一樣,所有的表結構信息我都用Row來表示。
  6. DataSet是強類型的。比如可以有Dataset[Car],Dataset[Person].
  7. DataFrame只是知道字段,但是不知道字段的類型,所以在執行這些操作的時候是沒辦法在編譯的時候檢查是否類型失敗的,比如你可以對一個String進行減法操作,在執行的時候才報錯,而DataSet不僅僅知道字段,而且知道字段類型,所以有更嚴格的錯誤檢查。就跟JSON對象和類對象之間的類比。

1.3.4 三者的共性

1、RDD、DataFrame、Dataset全都是spark平臺下的分佈式彈性數據集,爲處理超大型數據提供便利

2、三者都有惰性機制,在進行創建、轉換,如map方法時,不會立即執行,只有在遇到Action如foreach時,三者纔會開始遍歷運算,極端情況下,如果代碼裏面有創建、轉換,但是後面沒有在Action中使用對應的結果,在執行時會被直接跳過。

val sparkconf = new SparkConf().setMaster("local").setAppName("test")

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進行操作許多操作都需要這個包進行支持

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.3.5 三者的區別

RDD:

1、RDD一般和spark mlib同時使用

2、RDD不支持sparksql操作

DataFrame:

1、與RDD和Dataset不同,DataFrame每一行的類型固定爲Row,只有通過解析才能獲取各個字段的值,每一列的值沒法直接訪問,如

testDF.foreach{ line =>

    val col1=line.getAs[String]("col1")

    val col2=line.getAs[String]("col2")

}

2、DataFrame與Dataset一般不與spark ml同時使用

3、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)

4、DataFrame與Dataset支持一些特別方便的保存方式,比如保存成csv,可以帶上表頭,這樣每一列的字段名一目瞭然

//保存

val saveoptions = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://master01:9000/test")

datawDF.write.format("com.bigdata.spark.csv").mode(SaveMode.Overwrite).options(saveoptions).save()

//讀取

val options = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://master01:9000/test")

val datarDF= spark.read.options(options).format("com.bigdata.spark.csv").load()

利用這樣的保存方式,可以方便的獲得字段名和列的對應,而且分隔符(delimiter)可以自由指定。

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]就能比較好的解決問題

2 Spark SQL編程

2.1 spark-shell編程

例子:查詢年齡大於20歲的用戶,創建如下JSON格式的文件

{"name":"jack", "age":22}
{"name":"rose", "age":21}
{"name":"mike", "age":19}

2.2 IDEA創建Spark SQL 程序

IDEA中程序的打包和運行都和SparkCore類似,在Maven依賴中導入新的依賴項:

<!--導入spark sql的依賴jar包-->
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-sql_2.11</artifactId>
    <version>2.2.0</version>
</dependency>

程序如下:

import org.apache.spark.sql.{DataFrame, SQLContext}
import org.apache.spark.{SparkConf, SparkContext}

object SaprkSqlFirstDemo {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
      .setMaster("local")
      .setAppName(this.getClass.getSimpleName)
    val sc = new SparkContext(conf)
    //操作sql的專用實例
    val sQLContext = new SQLContext(sc)

    //導入sql實例上的隱式轉換
    import sQLContext.implicits._

    val df: DataFrame = sQLContext.read.json("e://student.json")
    df.show()

    df.filter("age > 20").show()

    df.createTempView("student")
    sQLContext.sql("select name,age from student where age > 20").show()

    sc.stop()
  }
}

3 Spark SQL解析

3.1 新的起始點SparkSession

在老的版本中,SparkSQL提供兩種SQL查詢起始點,一個叫SQLContext,用於Spark自己提供的SQL查詢,一個叫HiveContext,用於連接Hive的查詢,SparkSession是Spark最新的SQL查詢起始點,實質上是SQLContext和HiveContext的組合,所以在SQLContext和HiveContext上可用的API在SparkSession上同樣是可以使用的。SparkSession內部封裝了sparkContext,所以計算實際上是由sparkContext完成的。

val spark = SparkSession.builder()
  .master("local[*]")
  .appName(this.getClass.getSimpleName)
  .getOrCreate()

import spark.implicits._

SparkSession.builder 用於創建一個SparkSession。

import spark.implicits._的引入是用於將DataFrames隱式轉換成RDD,使df能夠使用RDD中的方法。

如果需要Hive支持的話,則需要創建以下語句

val spark = SparkSession.builder()
  .master("local[*]")
  .appName(this.getClass.getSimpleName)
  .enableHiveSupport() //開啓spark對hive的支持
  .getOrCreate()


import spark.implicits._

3.2 創建DataFrame

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

從已有的RDD進行轉換

/*
1001 zhangshan male
1002 lishi female
1003 zhaoliu male
*/
val file: RDD[String] = sc.textFile("spark_day01/student.txt")

val df: DataFrame = file.map(line => {
  val fiels: Array[String] = line.split(" ")
  (fiels(0).toInt, fiels(1), fiels(2))
}).toDF("id", "name", "sex")
df.show()

其他兩種方式我們在後面的數據源介紹

3.3 DataFrame的常用操作

3.3.1 DSL(Domain Specific Language)風格語法

people.txt

tom 18 87

jack 19 88

rose 20 95

import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.{DataFrame, Dataset, SparkSession}

object DatasetDSLDemo {
  // 配置日誌的顯示級別
  Logger.getLogger("org").setLevel(Level.ERROR)

  def main(args: Array[String]): Unit = {

    val spark: SparkSession = SparkSession.builder().master("local[*]")
      .appName(this.getClass.getSimpleName)
      .getOrCreate()

    // 導入隱式轉換
    import spark.implicits._  

    val file: Dataset[String] = spark.read.textFile("hdfs://hadoop101:9000/people.txt")

    val tpDs: Dataset[(String, Int, Int)] = file.map(t => {
      val split = t.split(" ")
      (split(0), split(1).toInt, split(2).toInt)
    })

    val pdf: DataFrame = tpDs.toDF("name", "age", "fv")

    //    ("select age from v_person where age > 23 ")

    // DSL 對應着sql中的一些操作
    // select 選擇
    pdf.select("age","name").show()

    // where 是sql中的寫法  filter   where 調用的就是filter
    pdf.where("age > 18").show()

    // 排序 orderBy  sort 同一個API  默認是升序
    pdf.orderBy($"age" desc).show()

    // 分組聚合
    // 統計次數 count(*)
    pdf.groupBy("age").count().show()

    // 導入函數
    import org.apache.spark.sql.functions._
    pdf.groupBy("age").agg(count("*") as "cnts").show()

    val sumfv  = pdf.groupBy("age").agg(sum("fv"))
    sumfv.show()

    pdf.groupBy("age").agg(min("fv") as "minfv").orderBy("minfv").show()
    sumfv.printSchema()

    // 如何修改字段的名稱
    sumfv.withColumnRenamed("sum(fv)","sumFV").show()

    //實際 優先使用SQl 語法風格,一些簡單的可以用DSL
    spark.stop()
  }
}

3.3.2 SQL風格語法

import org.apache.spark.SparkConf
import org.apache.spark.sql.{DataFrame, Dataset, SparkSession}

object DatasetSQLDemo {
  def main(args: Array[String]): Unit = {
    //兩種方式獲取sparkSession實例
    val spark = SparkSession.builder().master("local").appName(this.getClass.getSimpleName).getOrCreate()

    /*val conf = new SparkConf().setMaster("local").setAppName(this.getClass.getSimpleName)
    val spark = SparkSession.builder().config(conf).getOrCreate()*/

    //在sparkSession中,已經創建好了sqlContext和sparkContext
    val sqlContext = spark.sqlContext
    val sc = spark.sparkContext

    import spark.implicits._
    /* 當沒有隱式轉換時出現下面的錯誤:缺少import spark.implicits._的支持
    Error:(24, 53) Unable to find encoder for type stored in a Dataset.
    Primitive types (Int, String, etc) and Product types (case classes) are supported by importing spark.implicits._
    Support for serializing other types will be added in future releases.
    val tpDS: Dataset[(String, Int, Int)] = file.map(t => {
    */

    //讀取外部文件獲取到Dataset,在Dataset中自帶了schema信息
    val file: Dataset[String] = spark.read.textFile("hdfs://hadoop101:9000/people.txt")
    file.printSchema()


    val tpDS: Dataset[(String, Int, Int)] = file.map(t => {
      val strings = t.split(" ")
      (strings(0), strings(1).toInt, strings(2).toInt)
    })
    tpDS.printSchema()

    val pdf: DataFrame = tpDS.toDF("name","age","fv")
    pdf.printSchema()

    //註冊臨時試圖,使用範圍爲當前session,session退出後,表就失效了。
    //可以註冊爲全局表,但是當使用全局表時,需要全路徑訪問,如global_temp.people
    pdf.createTempView("v_people")
    pdf.createGlobalTempView("people")
    
    //寫sql
    val result = spark.sql("select name, age, fv from v_people where age > 18")
    val result1 = spark.sql("select name, age, fv from global_temp.people where age > 18"

    result.show()
    result1.show()

    spark.close()
  }
}

3.4 創建Dataset

Dataset是強類型的數據集合,需要提供對應的數據類型信息

import org.apache.spark.sql.{Dataset, SparkSession}

object CreateDataset {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .master("local[*]")
      .appName(this.getClass.getSimpleName)
      .getOrCreate()

    import spark.implicits._

    val caseClassDS: Dataset[Person] = Seq(Person("jack","18")).toDS()
    caseClassDS.show()

    val primitiveDS: Dataset[Int] = Seq(1, 2, 3).toDS()
    primitiveDS.map(_ + 1).collect().foreach(println(_))

    val peopleDS = spark.read.json("hdfs://hadoop101:9000/WordCount/student.json").as[Person]
    peopleDS.show()
  }
}
case class Person(name: String, age: String)

3.5 Dataset和RDD互操作

Spark SQL支持通過兩種方式將存在的RDD轉換爲Dataset,轉換的過程中需要讓Dataset獲取RDD中的Schema信息,主要有兩種方式,一種是通過反射來獲取RDD中的Schema信息。這種方式適合於列名已知的情況下。第二種是通過編程接口的方式將Schema信息應用於RDD,這種方式可以處理那種在運行時才能知道列的方式。

3.5.1 通過反射獲取Scheam

SparkSQL能夠自動將包含有case類的RDD轉換成DataFrame,case類定義了table的結構,之前我們都是使用case類的方式,將case類屬性通過反射變成了表的列名。Case類可以包含諸如Seqs或者Array等複雜的結構。

3.5.2 通過編程設置schema

如果case類不能夠提前定義,可以通過下面三個步驟定義一個DataFrame

創建一個多行結構的RDD;

創建用StructType來表示的行結構信息。

通過SparkSession提供的createDataFrame方法來應用Schema 。

import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types._

object CreateBySetSchema {
  def main(args: Array[String]): Unit = {
    val spark: SparkSession = SparkSession.builder()
      .master("local[*]")
      .appName("CreateBySetSchema")
      .getOrCreate()

    val peopleRDD: RDD[String] = spark.sparkContext.textFile("hdfs://hadoop101:9000/people.txt")

    //創建schema信息
    val schemaDemo = StructType(
      Seq(
        StructField("name", StringType),
        StructField("age", IntegerType),
        StructField("fv", IntegerType)
    ))

    import org.apache.spark.sql._
    //將讀取到的數據切分好字段,轉換爲Row
    val rowRDD = peopleRDD
      .map(_.split(" "))
      .map(attributes => Row(attributes(0), attributes(1).toInt,attributes(2).toInt))

    //將創建的schema信息和Row傳入即可創建DF
    val peopleDF: DataFrame = spark.createDataFrame(rowRDD,schemaDemo)
    peopleDF.createTempView("people")

    val result: DataFrame = spark.sql("select * from people")
    result.show()
  }
}

3.6 類型之間的轉換總結

RDD、DataFrame、Dataset三者有許多共性,有各自適用的場景常常需要在三者之間轉換

3.6.1 DataFrame/Dataset轉RDD:

這個轉換很簡單

val rdd1=testDF.rdd

val rdd2=testDS.rdd

3.6.2 RDD轉DataFrame:

import spark.implicits._

val testDF = rdd.map { line => (line._1,line._2) }.toDF("col1","col2")

一般用元組把一行的數據寫在一起,然後在toDF中指定字段名

3.6.3 RDD轉Dataset:

import spark.implicits._

case class Coltest(col1:String,col2:Int)extends Serializable //定義字段名和類型

val testDS = rdd.map {line=>

      Coltest(line._1,line._2)

    }.toDS

可以注意到,定義每一行的類型(case class)時,已經給出了字段名和類型,後面只要往case class裏面添加值即可

3.6.4 Dataset轉DataFrame

這個也很簡單,因爲只是把case class封裝成Row

import spark.implicits._

val testDF = testDS.toDF

3.6.5 DataFrame轉Dataset:

import spark.implicits._

case class Coltest(col1:String,col2:Int)extends Serializable //定義字段名和類型

val testDS = testDF.as[Coltest]

這種方法就是在給出每一列的類型後,使用as方法,轉成Dataset,這在數據類型是DataFrame又需要針對各個字段處理時極爲方便。

在使用一些特殊的操作時,一定要加上 import spark.implicits._ 不然toDF、toDS無法使用

3.7 用戶自定義函數

3.7.1 用戶自定義UDF函數

import org.apache.spark.sql.{DataFrame, SparkSession}

object DefFunctionUDF {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .master("local[*]")
      .appName(this.getClass.getSimpleName)
      .getOrCreate()

    import spark.implicits._

    val df: DataFrame = spark.read.json("hdfs://hadoop101:9000/WordCount/student.json")
    df.show()

    //自定義函數並註冊
    spark.udf.register("addName",(x: String) => "Name:" + x)

    df.createTempView("student")
    //使用自定函數
    spark.sql("select addName(name),age from student").show()
    /*
    +-----------------+---+
    |UDF:addName(name)|age|
    +-----------------+---+
    |        Name:jack| 22|
    |        Name:rose| 21|
    |        Name:mike| 19|
    +-----------------+---+
    */
  }
}

3.7.2 用戶自定義聚合函數

強類型的Dataset和弱類型的DataFrame都提供了相關的聚合函數, 如 count(),countDistinct(),avg(),max(),min()。除此之外,用戶可以設定自己的自定義聚合函數。

1. 弱類型用戶自定義聚合函數:通過繼承UserDefinedAggregateFunction來實現用戶自定義聚合函數。下面展示一個求平均工資的自定義聚合函數。

salary.json

{"name":"jack", "salary":1500}
{"name":"rose", "salary":1300}
{"name":"mike", "salary":1800}
{"name":"jone", "salary":1600}

import org.apache.spark.sql.expressions.MutableAggregationBuffer
import org.apache.spark.sql.expressions.UserDefinedAggregateFunction
import org.apache.spark.sql.types._
import org.apache.spark.sql.Row
import org.apache.spark.sql.SparkSession

object MyAverage extends UserDefinedAggregateFunction {
  // 聚合函數輸入參數的數據類型
  def inputSchema: StructType = StructType(StructField("inputColumn", LongType) :: Nil)

  // 聚合緩衝區中值得數據類型
  def bufferSchema: StructType = {
    StructType(StructField("sum", LongType) :: StructField("count", LongType) :: Nil)
  }

  // 返回值的數據類型
  def dataType: DataType = DoubleType

  // 對於相同的輸入是否一直返回相同的輸出。
  def deterministic: Boolean = true

  // 初始化
  def initialize(buffer: MutableAggregationBuffer): Unit = {
    // 存工資的總額
    buffer(0) = 0L
    // 存工資的個數
    buffer(1) = 0L
  }

  // 相同Execute間的數據合併。
  def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
    if (!input.isNullAt(0)) {
      buffer(0) = buffer.getLong(0) + input.getLong(0)
      buffer(1) = buffer.getLong(1) + 1
    }
  }

  // 不同Execute間的數據合併
  def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
    buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0)
    buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)
  }

  // 計算最終結果
  def evaluate(buffer: Row): Double = buffer.getLong(0).toDouble / buffer.getLong(1)
}

import org.apache.spark.sql.SparkSession

object DefFunctionText {

  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .master("local[*]")
      .appName(this.getClass.getSimpleName)
      .getOrCreate()

    import spark.implicits._
    // 註冊函數
    spark.udf.register("myAverage", MyAverage)

    val df = spark.read.json("e://salary.json")
    df.createOrReplaceTempView("employees")
    df.show()
    // +----+------+
    // |name|salary|
    // +----+------+
    // |jack|  1500|
    // |rose|  1300|
    // |mike|  1800|
    // |jone|  1600|
    // +----+------+

    val result = spark.sql("SELECT myAverage(salary) as average_salary FROM employees")
    result.show()
    // +--------------+
    // |average_salary|
    // +--------------+
    // |        1550.0|
    // +--------------+
  }
}

2. 強類型用戶自定義聚合函數:通過繼承Aggregator來實現強類型自定義聚合函數,同樣是求平均工資

import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.Encoder
import org.apache.spark.sql.Encoders
import org.apache.spark.sql.SparkSession

// 既然是強類型,可能有case類
case class Employee(name: String, salary: Long)

case class Average(var sum: Long, var count: Long)

object MyAverage1 extends Aggregator[Employee, Average, Double] {
  // 定義一個數據結構,保存工資總數和工資總個數,初始都爲0
  def zero: Average = Average(0L, 0L)

  // Combine two values to produce a new value. For performance, the function may modify `buffer`
  // and return it instead of constructing a new object
  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
}
import org.apache.spark.sql.{Dataset, SparkSession, TypedColumn}

object DefFunctionText2 {

  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .master("local[*]")
      .appName(this.getClass.getSimpleName)
      .getOrCreate()
    //導入隱式轉換
    import spark.implicits._

    //創建Dataset
    val ds: Dataset[Employee] = spark.read.json("e://salary.json").as[Employee]
    ds.createOrReplaceTempView("employees")
    ds.show()
    // +----+------+
    // |name|salary|
    // +----+------+
    // |jack|  1500|
    // |rose|  1300|
    // |mike|  1800|
    // |jone|  1600|
    // +----+------+

    val averageSalary: TypedColumn[Employee, Double] = MyAverage1.toColumn.name("average_salary")
    val result = ds.select(averageSalary)
    result.show()
    // +--------------+
    // |average_salary|
    // +--------------+
    // |        1550.0|
    // +--------------+
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章