大數據之Spark(5)- SparkSql

1 Spark SQL概述

1.1 什么是Spark SQL

Spark SQL是Spark用來處理結構化數據的一個模塊,它提供了2個編程抽象:DataFrame和DataSet,并且作為分布式SQL查詢引擎的作用。
我們已經學習了Hive,它是將Hive SQL轉換成MapReduce然后提交到集群上執行,大大簡化了編寫MapReduc的程序的復雜性,由于MapReduce這種計算模型執行效率比較慢。所有Spark SQL的應運而生,它是將Spark SQL轉換成RDD,然后提交到集群執行,執行效率非常快!

1.2 Spark SQL的特點

1)易整合

2)統一的數據訪問方式

3)兼容Hive

4)標準的數據連接

1.3 什么是DataFrame

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

上圖直觀地體現了DataFrame和RDD的區別。左側的RDD[Person]雖然以Person為類型參數,但Spark框架本身不了解Person類的內部結構。而右側的DataFrame卻提供了詳細的結構信息,使得Spark SQL可以清楚地知道該數據集中包含哪些列,每列的名稱和類型各是什么。DataFrame是為數據提供了Schema的視圖。可以把它當做數據庫中的一張表來對待,DataFrame也是懶執行的。性能上比RDD要高,主要原因:
優化的執行計劃:查詢計劃通過Spark catalyst optimiser進行優化。

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

1.4 什么是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對象和類對象之間的類比。

2 SparkSQL編程

2.1 SparkSession

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

2.2 DataFrame

2.2.1 創建

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

scala> spark.read.
csv   format   jdbc   json   load   option   options   orc   parquet   schema   table   text   textFile

(2)讀取json文件創建DataFrame

scala> val df = spark.read.json("/opt/modules/spark/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

(3)展示結果

scala> df.show
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+

2.2.2 SQL風格語法

1)創建一個DataFrame

scala> val df = spark.read.json("/opt/modules/spark/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

2)對DataFrame創建一個臨時表

scala> df.createOrReplaceTempView("people")

3)通過SQL語句實現查詢全表

scala> val sqlDF = spark.sql("SELECT * FROM people")
sqlDF: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

4)結果展示

scala> sqlDF.show
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+

注意:臨時表是Session范圍內的,Session退出后,表就失效了。如果想應用范圍內有效,可以使用全局表。注意使用全局表時需要全路徑訪問,如:global_temp.people
5)對于DataFrame創建一個全局表

scala> df.createGlobalTempView("people")

6)通過SQL語句實現查詢全表

scala> spark.sql("SELECT * FROM global_temp.people").show()
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|

scala> spark.newSession().sql("SELECT * FROM global_temp.people").show()
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+

2.2.3 DSL風格語法

1)創建一個DateFrame

scala> val df = spark.read.json("/opt/modules/spark/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

2)查看DataFrame的Schema信息

scala> df.printSchema
root
 |-- age: long (nullable = true)
 |-- name: string (nullable = true)

3)只查看”name”列數據

scala> df.select("name").show()
+-------+
|   name|
+-------+
|Michael|
|   Andy|
| Justin|
+-------+

4)查看”name”列數據以及”age+1”數據

scala> df.select($"name", $"age" + 1).show()
+-------+---------+
|   name|(age + 1)|
+-------+---------+
|Michael|     null|
|   Andy|       31|
| Justin|       20|
+-------+---------+

5)查看”age”大于”21”的數據

scala> df.filter($"age" > 21).show()
+---+----+
|age|name|
+---+----+
| 30|Andy|
+---+----+

6)按照”age”分組,查看數據條數

scala> df.groupBy("age").count().show()
+----+-----+
| age|count|
+----+-----+
|  19|     1|
|null|     1|
|  30|     1|
+----+-----+

2.2.4 RDD轉換為DateFrame

注意:如果需要RDD與DF或者DS之間操作,那么都需要引入 import spark.implicits._ 【spark不是包名,而是sparkSession對象的名稱】
前置條件:導入隱式轉換并創建一個RDD

scala> import spark.implicits._
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)

(2)根據樣例類將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]

3)通過編程的方式
(1)導入所需的類型

scala> import org.apache.spark.sql.types._
import org.apache.spark.sql.types._

(2)創建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))

(3)導入所需的類型

scala> import org.apache.spark.sql.Row
import org.apache.spark.sql.Row

(4)根據給定的類型創建二元組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

(5)根據數據及給定的schema創建DataFrame

scala> val dataFrame = spark.createDataFrame(data, structType)
dataFrame: org.apache.spark.sql.DataFrame = [name: string, age: int]

2.2.5 DateFrame轉換為RDD

直接調用rdd即可
1)創建一個DataFrame

scala> val df = spark.read.json("/opt/moduleS/spark/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

2)將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

3)打印RDD

scala> dfToRDD.collect
res13: Array[org.apache.spark.sql.Row] = Array([Michael, 29], [Andy, 30], [Justin, 19])

2.3 DataSet

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

2.3.1 創建

1)創建一個樣例類

scala> case class Person(name: String, age: Long)
defined class Person

2)創建DataSet

scala> val caseClassDS = Seq(Person("Andy", 32)).toDS()
caseClassDS: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]

2.3.2 RDD轉換為DataSet

SparkSQL能夠自動將包含有case類的RDD轉換成DataFrame,case類定義了table的結構,case類屬性通過反射變成了表的列名。
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

2)創建一個樣例類

scala> case class Person(name: String, age: Long)
defined class Person

3)將RDD轉化為DataSet

scala> peopleRDD.map(line => {val para = line.split(",");Person(para(0),para(1).trim.toInt)}).toDS()

2.3.3 DataSet轉換為RDD

調用rdd方法即可。
1)創建一個DataSet

scala> val DS = Seq(Person("Andy", 32)).toDS()
DS: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]

2)將DataSet轉換為RDD

scala> DS.rdd
res11: org.apache.spark.rdd.RDD[Person] = MapPartitionsRDD[15] at rdd at <console>:28

2.4 DataFrame與DataSet的互操作

  1. 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]

2)創建一個樣例類

scala> case class Person(name: String, age: Long)
defined class Person

3)將DateFrame轉化為DataSet

scala> df.as[Person]
res14: org.apache.spark.sql.Dataset[Person] = [age: bigint, name: string]
  1. DataSet轉換為DataFrame
    1)創建一個樣例類
scala> case class Person(name: String, age: Long)
defined class Person

2)創建DataSet

scala> val ds = Seq(Person("Andy", 32)).toDS()
ds: org.apache.spark.sql.Dataset[Person] = [name: string, age: bigint]

3)將DataSet轉化為DataFrame

scala> val df = ds.toDF
df: org.apache.spark.sql.DataFrame = [name: string, age: bigint]

4)展示

scala> df.show
+----+---+
|name|age|
+----+---+
|Andy| 32|
+----+---+

2.4.1 DataSet轉DataFrame

這個很簡單,因為只是把case class封裝成Row
(1)導入隱式轉換

import spark.implicits._

(2)轉換

val testDF = testDS.toDF

2.4.2 DataFrame轉DataSet

(1)導入隱式轉換

import spark.implicits._

(2)創建樣例類

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

(3)轉換

val testDS = testDF.as[Coltest]

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

2.5 RDD、DataFrame、DataSet

在SparkSQL中Spark為我們提供了兩個新的抽象,分別是DataFrame和DataSet。他們和RDD有什么區別呢?首先從版本的產生上來看:
RDD (Spark1.0) —> Dataframe(Spark1.3) —> Dataset(Spark1.6)
如果同樣的數據都給到這三個數據結構,他們分別計算之后,都會給出相同的結果。不同是的他們的執行效率和執行方式。
在后期的Spark版本中,DataSet會逐步取代RDD和DataFrame成為唯一的API接口。

2.5.1 三者的共性

1、RDD、DataFrame、Dataset全都是spark平臺下的分布式彈性數據集,為處理超大型數據提供便利
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 _=>
        ""
    }

2.5.2 三者的區別

  1. RDD:
    1)RDD一般和spark mlib同時使用
    2)RDD不支持sparksql操作
  2. 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 mlib同時使用
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://cluster/test")
datawDF.write.format("com.jackyan.spark.csv").mode(SaveMode.Overwrite).options(saveoptions).save()
//讀取
val options = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://hadoop102:9000/test")
val datarDF= spark.read.options(options).format("com.jackyan.spark.csv").load()

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

  1. Dataset:
    1)Dataset和DataFrame擁有完全相同的成員函數,區別只是每一行的數據類型不同。
    2)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.6 IDEA創建SparkSQL程序

IDEA中程序的打包和運行方式都和SparkCore類似,Maven依賴中需要添加新的依賴項:

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-sql_2.11</artifactId>
    <version>2.4.8</version>
</dependency>

程序如下:

object HelloWorld {

  def main(args: Array[String]) {
    //創建SparkConf()并設置App名稱
    val spark = SparkSession
      .builder()
      .appName("Spark SQL basic example")
      .config("spark.some.config.option", "some-value")
      .getOrCreate()

    // For implicit conversions like converting RDDs to DataFrames
    import spark.implicits._

    val df = spark.read.json("data/people.json")

    // Displays the content of the DataFrame to stdout
    df.show()

    df.filter($"age" > 21).show()

    df.createOrReplaceTempView("persons")

    spark.sql("SELECT * FROM persons where age > 21").show()

    spark.stop()
  }
}

2.7 用戶自定義函數

在Shell窗口中可以通過spark.udf功能用戶可以自定義函數。

2.7.1 用戶自定義UDF函數

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|
+----+-------+

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")

scala> spark.sql("Select addName(name), age from people").show()
+-----------------+----+
|UDF:addName(name)| age|
+-----------------+----+
|     Name:Michael|null|
|        Name:Andy|  30|
|      Name:Justin|  19|
+-----------------+----+

2.7.2 用戶自定義聚合函數

強類型的Dataset和弱類型的DataFrame都提供了相關的聚合函數, 如 count(),countDistinct(),avg(),max(),min()。除此之外,用戶可以設定自己的自定義聚合函數。
弱類型用戶自定義聚合函數:通過繼承UserDefinedAggregateFunction來實現用戶自定義聚合函數。下面展示一個求平均工資的自定義聚合函數。

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)
}

// 注冊函數
spark.udf.register("myAverage", MyAverage)

val df = spark.read.json("examples/src/main/resources/employees.json")
df.createOrReplaceTempView("employees")
df.show()
// +-------+------+
// |   name|salary|
// +-------+------+
// |Michael|  3000|
// |   Andy|  4500|
// | Justin|  3500|
// |  Berta|  4000|
// +-------+------+

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

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

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

object MyAverage 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 spark.implicits._

val ds = spark.read.json("examples/src/main/resources/employees.json").as[Employee]
ds.show()
// +-------+------+
// |   name|salary|
// +-------+------+
// |Michael|  3000|
// |   Andy|  4500|
// | Justin|  3500|
// |  Berta|  4000|
// +-------+------+

// Convert the function to a `TypedColumn` and give it a name
val averageSalary = MyAverage.toColumn.name("average_salary")
val result = ds.select(averageSalary)
result.show()
// +--------------+
// |average_salary|
// +--------------+
// |        3750.0|
// +--------------+

3 SparkSQL數據源

3.1 通用加載/保存方法

3.1.1 手動指定選項

Spark SQL的DataFrame接口支持多種數據源的操作。一個DataFrame可以進行RDDs方式的操作,也可以被注冊為臨時表。把DataFrame注冊為臨時表之后,就可以對該DataFrame執行SQL查詢。
Spark SQL的默認數據源為Parquet格式。數據源為Parquet文件時,Spark SQL可以方便的執行所有的操作。修改配置項spark.sql.sources.default,可修改默認數據源格式。

val df = spark.read.load("examples/src/main/resources/users.parquet") df.select("name", "favorite_color").write.save("namesAndFavColors.parquet")

當數據源格式不是parquet格式文件時,需要手動指定數據源的格式。數據源格式需要指定全名(例如:org.apache.spark.sql.parquet),如果數據源格式為內置格式,則只需要指定簡稱定json, parquet, jdbc, orc, libsvm, csv, text來指定數據的格式。
可以通過SparkSession提供的read.load方法用于通用加載數據,使用write和save保存數據。

val peopleDF = spark.read.format("json").load("examples/src/main/resources/people.json")
peopleDF.write.format("parquet").save("hdfs://hadoop102:9000/namesAndAges.parquet")

除此之外,可以直接運行SQL在文件上:

val sqlDF = spark.sql("SELECT * FROM parquet.`hdfs://hadoop102:9000/namesAndAges.parquet`")
sqlDF.show()
scala> val peopleDF = spark.read.format("json").load("examples/src/main/resources/people.json")
peopleDF: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> peopleDF.write.format("parquet").save("hdfs://hadoop102:9000/namesAndAges.parquet")

scala> peopleDF.show()
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+

scala> val sqlDF = spark.sql("SELECT * FROM parquet.`hdfs:// hadoop102:9000/namesAndAges.parquet`")
17/09/05 04:21:11 WARN ObjectStore: Failed to get database parquet, returning NoSuchObjectException
sqlDF: org.apache.spark.sql.DataFrame = [age: bigint, name: string]

scala> sqlDF.show()
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+

3.1.2 文件保存選項

可以采用SaveMode執行存儲操作,SaveMode定義了對數據的處理模式。需要注意的是,這些保存模式不使用任何鎖定,不是原子操作。此外,當使用Overwrite方式執行時,在輸出新數據之前原數據就已經被刪除。SaveMode詳細介紹如下表:

Scala/Java Any Language Meaning
SaveMode.ErrorIfExists(default) "error"(default) 如果文件存在,則報錯
SaveMode.Append "append" 追加
SaveMode.Overwrite "overwrite" 覆寫
SaveMode.Ignore "ignore" 數據存在,則忽略

3.2 JSON文件

Spark SQL 能夠自動推測 JSON數據集的結構,并將它加載為一個Dataset[Row]. 可以通過SparkSession.read.json()去加載一個 一個JSON 文件。
注意:這個JSON文件不是一個傳統的JSON文件,每一行都得是一個JSON串。

{"name":"Michael"}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19}

// Primitive types (Int, String, etc) and Product types (case classes) encoders are
// supported by importing this when creating a Dataset.
import spark.implicits._

// A JSON dataset is pointed to by path.
// The path can be either a single text file or a directory storing text files
val path = "examples/src/main/resources/people.json"
val peopleDF = spark.read.json(path)

// The inferred schema can be visualized using the printSchema() method
peopleDF.printSchema()
// root
//  |-- age: long (nullable = true)
//  |-- name: string (nullable = true)

// Creates a temporary view using the DataFrame
peopleDF.createOrReplaceTempView("people")

// SQL statements can be run by using the sql methods provided by spark
val teenagerNamesDF = spark.sql("SELECT name FROM people WHERE age BETWEEN 13 AND 19")
teenagerNamesDF.show()
// +------+
// |  name|
// +------+
// |Justin|
// +------+

// Alternatively, a DataFrame can be created for a JSON dataset represented by
// a Dataset[String] storing one JSON object per string
val otherPeopleDataset = spark.createDataset(
"""{"name":"Yin","address":{"city":"Columbus","state":"Ohio"}}""" :: Nil)
val otherPeople = spark.read.json(otherPeopleDataset)
otherPeople.show()
// +---------------+----+
// |        address|name|
// +---------------+----+
// |[Columbus,Ohio]| Yin|

3.3 Parquet文件

Parquet是一種流行的列式存儲格式,可以高效地存儲具有嵌套字段的記錄。Parquet格式經常在Hadoop生態圈中被使用,它也支持Spark SQL的全部數據類型。Spark SQL 提供了直接讀取和存儲 Parquet 格式文件的方法。

import spark.implicits._

val peopleDF = spark.read.json("examples/src/main/resources/people.json")

peopleDF.write.parquet("hdfs://hadoop102:9000/people.parquet")

val parquetFileDF = spark.read.parquet("hdfs:// hadoop102:9000/people.parquet")

parquetFileDF.createOrReplaceTempView("parquetFile")

val namesDF = spark.sql("SELECT name FROM parquetFile WHERE age BETWEEN 13 AND 19")
namesDF.map(attributes => "Name: " + attributes(0)).show()
// +------------+
// |       value|
// +------------+
// |Name: Justin|
// +------------+

3.4 JDBC

Spark SQL可以通過JDBC從關系型數據庫中讀取數據的方式創建DataFrame,通過對DataFrame一系列的計算后,還可以將數據再寫回關系型數據庫中。
注意:需要將相關的數據庫驅動放到spark的類路徑下。
(1)啟動spark-shell

$ bin/spark-shell

(2)從Mysql數據庫加載數據方式一

val jdbcDF = spark.read
.format("jdbc")
.option("url", "jdbc:mysql://hadoop102:3306/rdd")
.option("dbtable", "rddtable")
.option("user", "root")
.option("password", "000000")
.load()

(3)從Mysql數據庫加載數據方式二

val connectionProperties = new Properties()
connectionProperties.put("user", "root")
connectionProperties.put("password", "root")
val jdbcDF2 = spark.read
.jdbc("jdbc:mysql://hadoop101:3306/rdd", "rddtable", connectionProperties)

(4)將數據寫入Mysql方式一

jdbcDF.write
.format("jdbc")
.option("url", "jdbc:mysql://hadoop101:3306/rdd")
.option("dbtable", "dftable")
.option("user", "root")
.option("password", "root")
.save()

(5)將數據寫入Mysql方式二

jdbcDF2.write
.jdbc("jdbc:mysql://hadoop102:3306/rdd", "db", connectionProperties)

3.5 Hive數據庫

Apache Hive是Hadoop上的SQL引擎,Spark SQL編譯時可以包含Hive支持,也可以不包含。包含Hive支持的Spark SQL可以支持Hive表訪問、UDF(用戶自定義函數)以及 Hive 查詢語言(HiveQL/HQL)等。需要強調的一點是,如果要在Spark SQL中包含Hive的庫,并不需要事先安裝Hive。一般來說,最好還是在編譯Spark SQL時引入Hive支持,這樣就可以使用這些特性了。如果你下載的是二進制版本的 Spark,它應該已經在編譯時添加了 Hive 支持。
若要把Spark SQL連接到一個部署好的Hive上,你必須把hive-site.xml復制到 Spark的配置文件目錄中($SPARK_HOME/conf)。即使沒有部署好Hive,Spark SQL也可以運行。 需要注意的是,如果你沒有部署好Hive,Spark SQL會在當前的工作目錄中創建出自己的Hive 元數據倉庫,叫作 metastore_db。此外,如果你嘗試使用 HiveQL 中的 CREATE TABLE (并非 CREATE EXTERNAL TABLE)語句來創建表,這些表會被放在你默認的文件系統中的 /user/hive/warehouse 目錄中(如果你的 classpath 中有配好的 hdfs-site.xml,默認的文件系統就是 HDFS,否則就是本地文件系統)。

3.5.1 內嵌Hive應用

如果要使用內嵌的Hive,什么都不用做,直接用就可以了。
可以通過添加參數初次指定數據倉庫地址:

--conf spark.sql.warehouse.dir=hdfs://hadoop101/spark-wearhouse

注意:如果你使用的是內部的Hive,在Spark2.0之后,spark.sql.warehouse.dir用于指定數據倉庫的地址,如果你需要是用HDFS作為路徑,那么需要將core-site.xml和hdfs-site.xml 加入到Spark conf目錄,否則只會創建master節點上的warehouse目錄,查詢時會出現文件找不到的問題,這是需要使用HDFS,則需要將metastore刪除,重啟集群。

3.5.2 外部Hive應用

如果想連接外部已經部署好的Hive,需要通過以下幾個步驟。
1)將Hive中的hive-site.xml拷貝或者軟連接到Spark安裝目錄下的conf目錄下。
2)打開spark shell,注意帶上訪問Hive元數據庫的JDBC客戶端

$ bin/spark-shell  --jars mysql-connector-java-5.1.49-bin.jar

3.5.3 運行Spark SQL CLI

Spark SQL CLI可以很方便的在本地運行Hive元數據服務以及從命令行執行查詢任務。在Spark目錄下執行如下命令啟動Spark SQL CLI:

./bin/spark-sql

3.5.4 代碼中使用Hive

(1)添加依賴:

<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-hive -->
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-hive_2.11</artifactId>
    <version>2.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.hive/hive-exec -->
<dependency>
    <groupId>org.apache.hive</groupId>
    <artifactId>hive-exec</artifactId>
    <version>1.2.1</version>
</dependency>

(2)創建SparkSession時需要添加hive支持

val warehouseLocation: String = new File("spark-warehouse").getAbsolutePath

val spark = SparkSession
.builder()
.appName("Spark Hive Example")
.config("spark.sql.warehouse.dir", warehouseLocation)
.enableHiveSupport()
.getOrCreate()

注意:使用內置Hive需要指定一個Hive倉庫地址。若使用的是外部Hive,則需要將hive-site.xml添加到ClassPath下。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容