《Spark The Definitive Guide》Chapter 5:基本結構化API操作

Chapter 5:基本結構化API操作

前言

《Spark 權威指南》學習計劃

Schemas (模式)

我這裏使用的是書附帶的數據源中的 2015-summary.csv 數據

scala> val df = spark.read.format("csv").option("header","true").option("inferSchema","true").load("data/2015-summary.csv")
df: org.apache.spark.sql.DataFrame = [DEST_COUNTRY_NAME: string, ORIGIN_COUNTRY_NAME: string ... 1 more field]

scala> df.printSchema
root
 |-- DEST_COUNTRY_NAME: string (nullable = true)
 |-- ORIGIN_COUNTRY_NAME: string (nullable = true)
 |-- count: integer (nullable = true)

通過printSchema方法打印df的Schema。這裏Schema的構造有兩種方式,一是像上面一樣讀取數據時根據數據類型推斷出Schema(schema-on-read),二是自定義Schema。具體選哪種要看你實際應用場景,如果你不知道輸入數據的格式,那就採用自推斷的。相反,如果知道或者在ETL清洗數據時就應該自定義Schema,因爲Schema推斷會根據讀入數據格式的改變而改變。

看下Schema具體是什麼,如下輸出可知自定義Schema要定義包含StructType和StructField兩種類型的字段,每個字段又包含字段名、類型、是否爲null或缺失

scala> spark.read.format("csv").load("data/2015-summary.csv").schema
res1: org.apache.spark.sql.types.StructType = StructType(StructField(DEST_COUNTRY_NAME,StringType,true), StructField(ORIGIN_COUNTRY_NAME,StringType,true), StructField(count,IntegerType,true))

一個自定義Schema的例子,具體就是先引入相關類StructType,StructField和相應內置數據類型(Chapter 4中提及的Spark Type),然後定義自己的Schema,最後就是讀入數據是通過schema方法指定自己定義的Schema

scala> import org.apache.spark.sql.types.{StructType,StructField,StringType,LongType}
import org.apache.spark.sql.types.{StructType, StructField, StringType, LongType}

scala> val mySchema = StructType(Array(
     |  StructField("DEST_COUNTRY_NAME",StringType,true),
     |  StructField("ORIGIN_COUNTRY_NAME",StringType,true),
     |  StructField("count",LongType,true)
     | ))
mySchema: org.apache.spark.sql.types.StructType = StructType(StructField(DEST_COUNTRY_NAME,StringType,true), StructField(ORIGIN_COUNTRY_NAME,StringType,true), StructField(count,LongType,true))

scala> val df = spark.read.format("csv").schema(mySchema).load("data/2015-summary.csv")
df: org.apache.spark.sql.DataFrame = [DEST_COUNTRY_NAME: string, ORIGIN_COUNTRY_NAME: string ... 1 more field]

scala> df.printSchema
root
 |-- DEST_COUNTRY_NAME: string (nullable = true)
 |-- ORIGIN_COUNTRY_NAME: string (nullable = true)
 |-- count: long (nullable = true)

看這裏StringType、LongType,其實就是Chapter 4中談過的Spark Type。還有就是上面自定義Schema真正用來的是把RDD轉換爲DataFrame,參見之前的筆記

Columns(列) 和 Expressions(表達式)

書提及這裏我覺得講得過多了,其實質就是告訴你在spark sql中如何引用一列。下面列出這些

df.select("count").show
df.select(df("count")).show
df.select(df.col("count")).show #col方法可用column替換,可省略df直接使用col
df.select($"count").show #scala獨有的特性,但性能沒有改進,瞭解即可(書上還提到了符號`'`也可以,如`'count`)
df.select(expr("count")).show
df.select(expr("count"),expr("count")+1 as "count+1").show(5) #as是取別名
df.select(expr("count+1")+1).show(5)
df.select(col("count")+1).show(5)

大致就上面這些了,主要是注意col和expr方法,二者的區別是expr可以直接把一個表達式的字符串作爲參數,即expr("count+1")等同於expr("count")+1expr("count")+1

多提一句,SQL中select * from xxx在spark sql中可以這樣寫df.select("*")/df.select(expr("*"))/df.select(col("*"))

書中這一塊還講了爲啥上面這三個式子相同,spark會把它們編譯成相同的語法邏輯樹,邏輯樹的執行順序相同。編譯原理學過吧,自上而下的語法分析,LL(1)自左推導
比如 (((col("someCol") + 5) * 200) - 6) < col("otherCol") 對應的邏輯樹如下

邏輯樹

Records(記錄) 和 Rows(行)

Chapter 4中談過DataFrame=DataSet[Row],DataFrame中的一行記錄(Record)就是一個Row類型的對象。Spark 使用列表達式 expression 操作 Row 對象,以產生有效的結果值。Row 對象的內部表示爲:字節數組。因爲我們使用列表達式操作 Row 對象,所以,字節數據不會對最終用戶展示(用戶不可見)

我們來自定義一個Row對象

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

scala> val myRow = Row("China",null,1,true)
myRow: org.apache.spark.sql.Row = [China,null,1,true]

首先要引入Row這個類,然後根據你的需要(對應指定的Schema)指定列的值和位置。爲啥說是對應Schema呢?明確一點,DataFrame纔有Schema,Row沒有,你之所以定義一個Row對象,不就是爲了轉成DataFrame嗎(後續可見將RDD轉爲DataFrame),不然RDD不能用嗎非得轉成Row,對吧。

訪問Row對象中的數據

scala> myRow(0)
res12: Any = China

scala> myRow.get
get          getByte    getDecimal   getInt       getLong   getShort    getTimestamp   
getAs        getClass   getDouble    getJavaMap   getMap    getString   getValuesMap   
getBoolean   getDate    getFloat     getList      getSeq    getStruct                  

scala> myRow.get(1)
res13: Any = null

scala> myRow.getBoolean(3)
res14: Boolean = true

scala> myRow.getString(0)
res15: String = China

scala> myRow(0).asInstanceOf[String]
res16: String = China

如上代碼,注意第二行輸入myRow.get提示了很多相應類型的方法

DataFrame 轉換操作(Transformations)

對應文檔:https://spark.apache.org/docs/2.4.0/api/scala/#org.apache.spark.sql.functions$,書中給的是2.2.0的,更新一下

書中談及了單一使用DataFrame時的幾大核心操作:

  • 添加行或列
  • 刪除行或列
  • 變換一行(列)成一列(行)
  • 根據列值對Rows排序

不同的Transformations

DataFrame創建

之前大體上是提及了一些創建方法的,像從數據源 json、csv、parquet 中創建,或者jdbc、hadoop格式的文件即可。還有就是從RDD轉化成DataFrame,這裏書上沒有細講,但可以看出就是兩種方式:通過自定義StructType創建DataFrame(編程接口)和通過case class 反射方式創建DataFrame(書中這一塊不明顯,因爲它只舉例了一個Row對象的情況)

參見我之前寫的:RDD如何轉化爲DataFrame

DataFrame還有一大優勢是轉成臨時視圖,可以直接使用SQL語言操作,如下:

df.createOrReplaceTempView("dfTable") #創建或替代臨時視圖
spark.sql("select * from dfTable where count>50").show

select 和 selectExpr

這兩個也很簡單就是SQL中的查詢語句select,區別在於select接收列 column 或 表達式 expression,selectExpr接收字符串表達式 expression

df.select(col("DEST_COUNTRY_NAME") as "dest_country").show(2)

spark.sql("select DEST_COUNTRY_NAME as `dest_country` from dfTable limit 2").show

你可以使用上文提及的Columns來替換col("DEST_COUNTRY_NAME")爲其他不同寫法,但要注意Columns對象不能和String字符串一起混用

scala> df.select(col("DEST_COUNTRY_NAME"),"EST_COUNTRY_NAME").show(2).show
<console>:26: error: overloaded method value select with alternatives:
  [U1, U2](c1: org.apache.spark.sql.TypedColumn[org.apache.spark.sql.Row,U1], c2: org.apache.spark.sql.TypedColumn[org.apache.spark.sql.Row,U2])org.apache.spark.sql.Dataset[(U1, U2)] <and>
  (col: String,cols: String*)org.apache.spark.sql.DataFrame <and>
  (cols: org.apache.spark.sql.Column*)org.apache.spark.sql.DataFrame
  
 cannot be applied to (org.apache.spark.sql.Column, String)
       df.select(col("DEST_COUNTRY_NAME"),"EST_COUNTRY_NAME").show(2).show

# cannot be applied to (org.apache.spark.sql.Column, String)

你也可以select多個列,逗號隔開就好了。如果你想給列名取別名的話,可以像上面 col("DEST_COUNTRY_NAME") as "dest_country"一樣,也可以 expr("DEST_COUNTRY_NAME as dest_country")(之前說過expr可以表達式的字符串)

Scala中還有一個操作是把更改別名後又改爲原來名字的,df.select(expr("DEST_COUNTRY_NAME as destination").alias("DEST_COUNTRY_NAME")).show(2),瞭解就好

而selectExpr就是簡化版的select(expr(xxx)),可以看成一種構建複雜表達式的簡單方法。到底用哪種,咱也不好說啥,咱也不好問,看自己情況吧,反正都可以使用

df.selectExpr("DEST_COUNTRY_NAME as destination","ORIGIN_COUNTRY_NAME").show(2)

# 聚合
scala> df.selectExpr("avg(count)","count(distinct(DEST_COUNTRY_NAME))").show(5)
+-----------+---------------------------------+                                 
| avg(count)|count(DISTINCT DEST_COUNTRY_NAME)|
+-----------+---------------------------------+
|1770.765625|                              132|
+-----------+---------------------------------+
# 等同於select的
scala> df.select(avg("count"),countDistinct("DEST_COUNTRY_NAME")).show()
+-----------+---------------------------------+                                 
| avg(count)|count(DISTINCT DEST_COUNTRY_NAME)|
+-----------+---------------------------------+
|1770.765625|                              132|
+-----------+---------------------------------+
# 等同於sql的
scala> spark.sql("SELECT avg(count), count(distinct(DEST_COUNTRY_NAME)) FROM dfTable
LIMIT 2")

轉換爲 Spark Types (Literals)

這裏我也搞不太明白它的意義在哪裏,書上說當你要比較一個值是否大於某個變量或者編程中創建的變量時會用到這個。然後舉了一個添加常數列1的例子

import org.apache.spark.sql.functions.lit
df.select(expr("*"), lit(1).as("One")).show(2)

-- in SQL
spark.sql(SELECT *, 1 as One FROM dfTable LIMIT 2)

實在是沒搞明白意義何在,比如說我查詢列count中大於其平均值的所有記錄

val result = df.select(avg("count")).collect()(0).getDouble(0)
df.where(col("count") > lit(result)).show() # 去掉lit也沒問題,所以,呵呵呵

添加或刪除列

DataFrame提供一個方法withColumn來添加列,如添加一個值爲1的列df.withColumn("numberOne",lit(1)),像極了pandas中的pd_df['numberOne'] = 1,不過withColumn是創建了新的DataFrame

還能通過實際的表達式賦予列值

scala> df.withColumn("withinCountry", expr("ORIGIN_COUNTRY_NAME ==DEST_COUNTRY_NAME")).show(2)
+-----------------+-------------------+-----+-------------+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|withinCountry|
+-----------------+-------------------+-----+-------------+
|    United States|            Romania|   15|        false|
|    United States|            Croatia|    1|        false|
+-----------------+-------------------+-----+-------------+
only showing top 2 rows

DataFrame提供了一個 drop 方法刪除列,其實學過R語言或者Python的話這裏很容易掌握,因爲像pandas裏都有一樣的方法。
drop這個方法也會創建新的DataFrame,不得不說雞肋啊,直接通過select也是一樣的效果

scala> df1.printSchema
root
 |-- DEST_COUNTRY_NAME: string (nullable = true)
 |-- ORIGIN_COUNTRY_NAME: string (nullable = true)
 |-- count: integer (nullable = true)
 |-- numberOne: integer (nullable = false)

# 刪除多個列就多個字段逗號隔開
scala> df1.drop("numberOne").columns
res52: Array[String] = Array(DEST_COUNTRY_NAME, ORIGIN_COUNTRY_NAME, count)

列名重命名

withColumnRenamed方法,如df.withColumnRenamed("DEST_COUNTRY_NAME","dest_country").columns,也是創建新DataFrame

保留字和關鍵字符

像列名中遇到空格或者破折號,可以使用單引號'括起,如下

dfWithLongColName.selectExpr("`This Long Column-Name`","`This Long Column-Name` as `new col`").show(2)

spark.sql("SELECT `This Long Column-Name`, `This Long Column-Name` as `new col` FROM dfTableLong LIMIT 2")

設置區分大小寫

默認spark大小寫不敏感的,但可以設置成敏感 spark.sql.caseSensitive屬性爲true即可

spark.sqlContext.setConf("spark.sql.caseSensitive","true")

這個意義並非在此,而是告訴你如何在程序中查看/設置自己想要配置的屬性。就SparkSession而言吧,spark.conf.setspark.conf.get即可,因爲SparkSession包含了SparkContext、SQLContext、HiveContext

更改列的類型

和Hive中更改類型一樣的,cast方法

scala> df1.withColumn("LongOne",col("numberOne").cast("Long")).printSchema
root
 |-- DEST_COUNTRY_NAME: string (nullable = true)
 |-- ORIGIN_COUNTRY_NAME: string (nullable = true)
 |-- count: integer (nullable = true)
 |-- numberOne: integer (nullable = false)
 |-- LongOne: long (nullable = false)

# 等同 SELECT *, cast(count as long) AS LongOne FROM dfTable

過濾Rows

就是where和filter兩個方法,選其一即可

scala> df.filter(col("DEST_COUNTRY_NAME")==="United States").filter($"count">2000).show
+-----------------+-------------------+------+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME| count|
+-----------------+-------------------+------+
|    United States|      United States|370002|
|    United States|             Mexico|  7187|
|    United States|             Canada|  8483|
+-----------------+-------------------+------+

//SQL寫法
spark.sql("select * from dfTable where DEST_COUNTRY_NAME=='United States' and count>2000").show

有一點要注意的是,等於和不等於的寫法:====!=

書中在這裏還提及了——在使用 Scala 或 Java 的 Dataset API 時,filter 還接受 Spark 將應用於數據集中每個記錄的任意函數

這裏補充一下,上面給出的示例是And條件判斷,那Or怎麼寫呢?

//SQL好寫
spark.sql("select * from dfTable where DEST_COUNTRY_NAME=='United States' and (count>200 or count<10)").show
//等價
df.filter(col("DEST_COUNTRY_NAME")==="United States").filter(expr("count>200").or(expr("count<10"))).show

//隨便舉個例子,還可以這樣創建個Column來比較
val countFilter = col("count") > 2000
val destCountryFilter1 = col("DEST_COUNTRY_NAME") === "United States"
val destCountryFilter2 = col("DEST_COUNTRY_NAME") === "China"
//取否加!
df.where(!countFilter).where(destCountryFilter1.or(destCountryFilter2)).groupBy("DEST_COUNTRY_NAME").count().show
+-----------------+-----+
|DEST_COUNTRY_NAME|count|
+-----------------+-----+
|    United States|  122|
|            China|    1|
+-----------------+-----+

Rows 去重

這個小標題可能有歧義,其實就是SQL中的distinct去重

//SQL
spark.sql("select COUNT(DISTINCT(ORIGIN_COUNTRY_NAME,DEST_COUNTRY_NAME)) FROM dfTable")
//df
df.select("ORIGIN_COUNTRY_NAME","DEST_COUNTRY_NAME").distinct.count

df 隨機取樣

scala> df.count
res1: Long = 256
# 種子
scala> val seed = 5
seed: Int = 5
# 是否替換原df
scala> val withReplacement = false
withReplacement: Boolean = false
# 抽樣比
scala> val fraction = 0.5
fraction: Double = 0.5
# sample
scala> df.sample(withReplacement,fraction,seed).count
res4: Long = 126

df 隨機切分

這個常用於機器學習做訓練集測試集切分(split),就好比是sklearn裏面的train_test_split。

def randomSplit(weights:Array[Double]):Array[org.apache.spark.sql.Dataset[org.apache.spark.sql.Row]]
def randomSplit(weights:Array[Double],seed:Long):Array[org.apache.spark.sql.Dataset[org.apache.spark.sql.Row]]

# 傳入Array指定切割比例,seed是種子
# 返回的也是Array類型
scala> val result = df.randomSplit(Array(0.25,0.75),5)
scala> result(0).count
res12: Long = 60

scala> result(1).count
res13: Long = 196

join 連接

怎麼說呢,spark sql提供的方法沒有SQL方式操作靈活簡便吧,看例子:

# df1用的上面得出的
df.join(df1,df.col("count")===df1.col("count")).show

inner join

默認內連接(inner join),從圖中可見相同字段沒有合併,而且重命名很難。你也可以如下用寫法

df.join(df1,"count").show
//多列join
df.join(df1,Seq("count","DEST_COUNTRY_NAME")).show

好處是相同字段合併了

union
還有就是左連接,右連接,外連接等等,在join方法中指明即可,如下

# 左外連接
df.join(df1,Seq("count","DEST_COUNTRY_NAME"),"leftouter").show

join type有以下可選:

Supported join types include: ‘inner’, ‘outer’, ‘full’, ‘fullouter’, ‘full_outer’, ‘leftouter’, ‘left’, ‘left_outer’, ‘rightouter’, ‘right’, ‘right_outer’, ‘leftsemi’, ‘left_semi’, ‘leftanti’, ‘left_anti’, ‘cross’.

我更推薦轉成臨時表,通過SQL方式寫起來簡便

union 合併

這個用來合併DataFrame(或DataSet),它不是按照列名和並得,而是按照位置合併的(所以DataFrame的列名可以不相同,但對應位置的列將合併在一起)。還有它這個和SQL中union 集合合併不等價(會去重),這裏的union不會去重

scala> val rows = Seq(
     | Row("New Country","Other Country",5),
     | Row("New Country2","Other Country3",1)
     | )
scala> val rdd = spark.sparkContext.parallelize(rows)
scala> import org.apache.spark.sql.types.{StructType,StructField}
scala> import org.apache.spark.sql.types.{StringType,IntegerType}
scala> val schema = StructType(Array(
     | StructField("dest_country",StringType,true),
     | StructField("origin_country",StringType,true),
     | StructField("count",IntegerType,true)
     | ))
scala> val newDF = spark.createDataFrame(rdd,schema)
scala> newDF.show
+------------+--------------+-----+
|dest_country|origin_country|count|
+------------+--------------+-----+
| New Country| Other Country|    5|
|New Country2|Other Country3|    1|
+------------+--------------+-----+

scala> newDF.printSchema
root
 |-- dest_country: string (nullable = true)
 |-- origin_country: string (nullable = true)
 |-- count: integer (nullable = true)


scala> df.printSchema
root
 |-- DEST_COUNTRY_NAME: string (nullable = true)
 |-- ORIGIN_COUNTRY_NAME: string (nullable = true)
 |-- count: long (nullable = true)
# 合併後的Schema,可見和列名無關
scala> df.union(newDF).printSchema
root
 |-- DEST_COUNTRY_NAME: string (nullable = true)
 |-- ORIGIN_COUNTRY_NAME: string (nullable = true)
 |-- count: long (nullable = true)

scala> df.union(newDF).where(col("DEST_COUNTRY_NAME").contains("New Country")).show
+-----------------+-------------------+-----+
|DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|
+-----------------+-------------------+-----+
|      New Country|      Other Country|    5|
|     New Country2|     Other Country3|    1|
+-----------------+-------------------+-----+

它不管你兩個DataFrame的Schema是否對上,只要求列數相同,至於Column的Type會向上轉型(即Integer可以向上轉爲String等)

scala> val df3 = df.select("ORIGIN_COUNTRY_NAME","count")
scala> df3.printSchema
root
 |-- ORIGIN_COUNTRY_NAME: string (nullable = true)
 |-- count: long (nullable = true)
# 要求列數匹配
scala> df1.union(df3)
org.apache.spark.sql.AnalysisException: Union can only be performed on tables with the same number of columns, but the first table has 3 columns and the second table has 2 columns;;

scala> val df4 = df3.withColumn("newCol",lit("spark"))
scala> df4.printSchema
root
 |-- ORIGIN_COUNTRY_NAME: string (nullable = true)
 |-- count: long (nullable = true)
 |-- newCol: string (nullable = false)
# 看最後的Column名和類型
scala> df.union(df4).printSchema
root
 |-- DEST_COUNTRY_NAME: string (nullable = true)
 |-- ORIGIN_COUNTRY_NAME: string (nullable = true)
 |-- count: string (nullable = true)

排序

spark sql提供sort和orderby兩個方法,都接受字符串、表達式、Columns對象參數,默認升序排序(Asc)

import org.apache.spark.sql.functions.{asc,desc}
df.sort("count").show(2)
df.sort(desc("count")).show(2)
df.sort(col("count").desc).show(2)
df.sort(expr("count").desc_nulls_first).show(2)
df.orderBy(desc("count"), asc("DEST_COUNTRY_NAME")).show(2)
# 下面這個我試着沒有用
df.orderBy(expr("count desc")).show(2)

注意,上面有一個屬性desc_nulls_first ,還有desc_nulls_last,同理asc也對應有兩個,這個用來指定排序時null數據是出現在前面還是後面

出於優化目的,有時建議在另一組轉換之前對每個分區進行排序。您可以使用 sortWithinPartitions 方法來執行以下操作:spark.read.format("json").load("/data/flight-data/json/*-summary.json").sortWithinPartitions("count")

前n個數據 (limit)

這個就像MySQL中取前n條數據一樣,select * from table limit 10;,spark sql也提供這麼一個方法df.limit(10).show

重分區

當你spark出現數據傾斜時,首先去UI查看是不是數據分佈不均,那就可以調整分區數,提高並行度,讓同一個key的數據分散開來,可以參考我之前寫的:MapReduce、Hive、Spark中數據傾斜問題解決歸納總結。Repartition 和 Coalesce方法可以用在這裏

def repartition(partitionExprs: org.apache.spark.sql.Column*)
def repartition(numPartitions: Int,partitionExprs: org.apache.spark.sql.Column*)
def repartition(numPartitions: Int): org.apache.spark.sql.Dataset[org.apache.spark.sql.Row]

看這三個方法,參數Columns是指對哪個列分區,numPartitions是分區數。還有repartition是對數據完全進行Shuffle的

# 重分區
df.repartition(col("DEST_COUNTRY_NAME"))
# 指定分區數
df.repartition(5, col("DEST_COUNTRY_NAME"))
# 查看分區數
df.rdd.getNumPartitions

而coalesce 是不會導致數據完全 shuffle的,並嘗試合併分區

df.repartition(5, col("DEST_COUNTRY_NAME")).coalesce(2)

將Rows返回給Driver程序

有以下幾個方法:collect、take、show,會將一些數據返回給Driver驅動程序,以便本地操作查看。

scala> df.take
   def take(n: Int): Array[org.apache.spark.sql.Row]
scala> df.takeAsList
   def takeAsList(n: Int): java.util.List[org.apache.spark.sql.Row]
scala> df.collectAsList
   def collectAsList(): java.util.List[org.apache.spark.sql.Row]
scala> df.collect
   def collect(): Array[org.apache.spark.sql.Row]

有一點是,collect謹慎使用,它會返回所有數據到本地,如果太大內存都裝不下,搞得driver崩潰。show方法這裏還能傳一個布爾型參數truncate,表示是否打印完全超過20字符的字符串(就是有些值太長了,是否完全打印)

還有一個方法 toLocalIterator 將分區數據作爲迭代器返回給驅動程序,以便迭代整個數據集,這個也會出現分區太大造成driver崩潰的出現

總結

這章講了下DataFrame基本的API使用和一些概念,下一章Chapter 6會更好地介紹如何使用不同方法來處理數據


收錄於此:josonle/Spark-The-Definitive-Guide-Learning

更多推薦:
Coding Now

學習記錄的一些筆記,以及所看得一些電子書eBooks、視頻資源和平常收納的一些自己認爲比較好的博客、網站、工具。涉及大數據幾大組件、Python機器學習和數據分析、Linux、操作系統、算法、網絡等

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