Spark入門之DataFrame/DataSet

目錄

本文代碼主要基於Spark2.2,Scala 2.11,Python3

由於用Scala和Python編寫的Spark application代碼十分類似,所以本文只展示Scala代碼,與Python不同的地方會說明。


Part I. Gentle Overview of Big Data and Spark

  1. Apache Spark is a unified computing engine and a set of libraries for parallel data processing on computer clusters.
  2. 大數據背景
    硬件散熱瓶頸 -> 多核CPU -> 並行計算
    數據儲存和收集成本不斷下降

Overview

1.基本架構

Spark driver管理和協調在集羣上的“任務”運作。這些集羣的資源由三種manager管理:standalone, YARN, or Mesos。他們有自己的master和workers來維護集羣運作。Spark driver向它們master請求分配Spark app中executors所需的物理資源(它們workers上的cpu,內存等)並啓動executors,然後Spark driver給這些executors分配tasks進行計算。

Application由a driver process and a set of executor processes構成。其中driver進程作爲核心,在一個節點上運行main(),並負責維護集羣的狀態、任務信息,規劃和分配工作給Executors。Executors負責執行代碼和反映情況。每個app有它自己的executor進程。

詳細參考“Spark深入之RDD”的Spark在集羣上運行或“Spark底層原理簡化版

2.基本概念

Spark有兩個基礎APIs集:非結構化的RDD和結構化的DataFrame/DataSet。

模塊組成:Spark Core(RDD), SQL(DF/DataSet), Structured Streaming, MLlib/ML等。

Starting Spark

spark-shell (or pyspark)直接進行交互式操作(比較少用,一般藉助下面的工具),而 spark-submit 一般是生成環境向集羣提交任務,如上面提到的yarn集羣。

交互式操作和調試:可使用jupyter notebook、zeppelin或spark notebook等,方便操作和可視化。

調試的代碼量大時用IDEA。

spark-submit的代碼詳細參考“Spark深入之RDD”開發開發Spark App。

入口

SparkSession 是一個driver線程。通過它的方法獲取操作對象。下面代碼調用它的range方法獲取操作對象(包含1-1000個數字的Dataset),並轉化爲DF,列名爲number。

val myRange = spark.range(1000).toDF("number")

對象(數據結構)

DataFrames (DF)相當於表格,通過schema 定義列及其數據類型。其他對象還有Datasets和RDDs。

這些對象都是被劃分爲多個partitions並分佈式地存儲在各個workers節點上。目前不能通過DF/DS設置partition(哪些數據放哪個partition),但RDD可以。另外它們都是 immutable 的(相當於final in Java)

節點、excutors進程、core線程、partition和block(HDFS的)的關係:

  • 一個節點可以有一個或多個excutors進程
  • 一個executor進程可以有n個core線程,m個partition(一個partition不能多個executors)
  • 一個core線程處理一個partition(一個task),partition過多就排隊等候core。
  • 一個partition一般由多份block數據合併而成(讀取HDFS數據時)

操作(transformation and action)

1.transformation 分爲 narrow 和 wide dependencies。
narrow (pipelining) : each input partition will contribute to only one output partition,即1 to 1(map)或n to 1(coalesce)。
wide (通常shuffle) : input partitions contributing to many output partitions,即 1 to n。

narrow操作可以預先知道數據分到哪,而不需要根據數據的key值來確定但wide相反,如sort, ByKey等需要repartition的算子。如果Spark已經根據partitioner知道數據按特定方式partitioned,就不一定shuffle。需要shuffle的,Spark會在該RDD上增加ShuffledDependency對象到它的依賴列表中。操作結果會寫到磁盤。這裏有很多優化的需求。

Lazy evaulation: 上述transformation的代碼在運行時不會被馬上執行,Spark會在action前對代碼的運行計劃進行優化後才運行。它能間接減少多次訪問數據(對訪問到的數據連續執行map和filter,而不是map完後在filter),還能使代碼更簡潔(相比於MapReduce,如下面代碼),Spark自動安排執行計劃。

//考慮StopWordsFilter的wordCount
def withStopWordsFiltered(rdd : RDD[String], illegalTokens : Array[Char],
    stopWords : Set[String]): RDD[(String, Int)] = {
    val separators = illegalTokens ++ Array[Char](' ')
    val tokens: RDD[String] = rdd.flatMap(_.split(separators).
      map(_.trim.toLowerCase))
    val words = tokens.filter(token =>
      !stopWords.contains(token) && (token.length > 0) )
    val wordPairs = words.map((_, 1))
    val wordCounts = wordPairs.reduceByKey(_ + _)
    wordCounts
}

2.action(trigger the computation)返回非核心數據結構,分爲to view data, to collect data 和 to output data。他們促發scheduler基於RDD的依賴關係,建立DAG。在執行DAG裏面的一系列步驟(stages)時,scheduler還能保持每一個分區不丟失數據(丟失部分重算)。

有些操作即是transformation又是action,如sortByKey

DAG在Spark裏是Scheduler,如果連接集羣,配置參數或啓動job時出錯,會是DAG Scheduler errors,因爲job是由DAG處理的。DAG爲每一個job建立一個stages組成的graph,決定了運行每個task的位置,並將信息傳給TaskScheduler。TaskScheduler負責在集羣的 running tasks,並創建一個graph,裏面有各partitions的依賴。

UI

本地模式入口 http://localhost:4040。查看Spark運行的各種情況

3.例子(可跳過)

CSV半結構化數據

//DF的header和第一行數據
DEST_COUNTRY_NAME,ORIGIN_COUNTRY_NAME,count
United States,Romania,15

val data = spark//python省去類型val
  .read
  .option("inferSchema", "true")//自動推斷schema
  .option("header", "true")
  .csv("path")//可用.format("csv")
//python一樣,但每行後加“/”分隔。如上文所述,上面操作並不會馬上執行。Spark只通過前面幾行推斷schema

data.sort("count").explain()//查看explain plan,explain可對DF使用,暫不細說。
//設置spark,它默認輸出200個partition,下面設置爲5。合理的配置能提高效率。
spark.conf.set("spark.sql.shuffle.partitions", "5")
//一個完整的transformation和action任務
data2015.sort("count").take(2)

Spark編程是函數式的,意味它不會改變原始數據,且相同輸入會得到相同結果。

上述操作也可以用SQL語法實現,而且效率是一樣(一樣的explain plain)。data.createOrReplaceTempView("tableName") 可以將DF轉化爲表格或視圖。spark.sql還可以直接查詢路徑中的文件spark.sql("SELECT * FROM parquet.`path_to_parquet_file`")

一些慣例:

  • 處理數據時先用take或者sample(數據量大時小心)提取部分數據進行模擬處理,確定怎麼處理後纔用到整個數據上。當所編輯的內容複雜起來後才轉到IDEA
  • count後如果數據不大,可用.cache,(在下一次action時)把數據放到內存,而不是每次action都要Spark重新讀數據
val sqlWay = spark.sql("""
SELECT DEST_COUNTRY_NAME, count(1)
FROM tableName
GROUP BY DEST_COUNTRY_NAME
""")
val dataFrameWay = data
  .groupBy('DEST_COUNTRY_NAME)
  .count()
//對上面兩個變量調用explain,所得plan一樣

//count列的最值
spark.sql("SELECT max(count) from tableName").take(1)
data.select(max("count")).take(1)

//求count總和前5的國家
maxSql = spark.sql("""
    SELECT DEST_COUNTRY_NAME, sum(count) as destination_total
    FROM flight_data_2015
    GROUP BY DEST_COUNTRY_NAME
    ORDER BY sum(count) DESC
    LIMIT 5
    """)
maxSql.show()

data.groupBy("DEST_COUNTRY_NAME")//產生一個RelationalGroupedDataset,需要指明聚合方法(如下面的sum)才能查看結果
    .sum("count")
    .withColumnRenamed("sum(count)", "destination_total")
    .sort(desc("destination_total"))
    .limit(5)//action
    .show()

//每一步都會產生一個新的immutable DF(groupBy除外),可以通過UI的DAG查看各步驟

總結:

Spark 是分佈式編程模型,用戶對它設定transformations和action操作。多步transformations會產生 a directed acyclic graph(指令圖,把指令劃分爲stages和tasks) 。一個action操作在集羣啓動這些指令。

DataFrames and Datasets 是 transformations和action的操作對象。Transformation會創建新對象,而action還可以將對象轉換爲native language types。


Spark工具箱

1.Datasets: Type-Safe Structured APIs

用於寫特定類型的數據(java和scala)。用戶可以通過Dataset API將java/scala的類裝進DF(DF裏裝的是Row類型,它包括各種tabular data)。目前支持JavaBean pattern in Java and case classes in Scala。

Dataset是類型安全的,意味着Spark會記得數據的類型。而DF的row每次提取值都需要getAs來確定類型。

case class Flight(DEST_COUNTRY_NAME: String,
                  ORIGIN_COUNTRY_NAME: String,
                  count: BigInt)
val flightsDF = spark.read
  .parquet("...fileName.parquet/")
val flights = flightsDF.as[Flight]//df -> dataset,除了case class(Flight),也可以是包含spark type的tuple
//這樣就可以對數據進行操作,當用collect或take時,得到的是相應的類Flight,而不是Row

flights
  .filter(flight_row => flight_row.ORIGIN_COUNTRY_NAME != "Canada")//有filterNot 
  .map(fr => Flight(fr.DEST_COUNTRY_NAME, fr.ORIGIN_COUNTRY_NAME, fr.count + 5))
  .take(5)

2.Structured Streaming

對輸入的數據流分批計算

//零售商數據,假設按照天分批輸入
InvoiceNo,StockCode,Description,Quantity,InvoiceDate,UnitPrice,CustomerID,Country
536365,85123A,WHITE HANGING HEART T-LIGHT HOLDER,6,2010-12-01 08:26:00,2.55

//下面計算一天裏每位顧客的消費。(略過spark設置)
//讀取數據
val streamingDataFrame = spark.readStream//不同的read
    .schema(staticSchema)//staticSchema自己編寫,或從樣例中讀取數據,轉化爲視圖後調用.schema後獲得。有.printSchema方法
    .option("maxFilesPerTrigger", 1)//每次讀取文件的個數
    .format("csv")
    .option("header", "true")
    .load(".../*.csv")//某文件夾裏所有csv

//計算(全是transformation)
val purchaseByCustomerPerHour = streamingDataFrame
  .selectExpr(
    "CustomerId",
    "(UnitPrice * Quantity) as total_cost",
    "InvoiceDate")
  .groupBy($"CustomerId", window($"InvoiceDate", "1 day"))//scala特色$“”在python中用col("")。注意$只引用其後緊貼的一個String expression,也可用‘InvoiceDate表示
  .sum("total_cost")

//輸出
purchaseByCustomerPerHour.writeStream
    .format("memory") // memory 表格存在內存
    .queryName("customer_purchases") // 表格名
    .outputMode("complete") // complete 所有count都在表格裏
    .start()

//查看結果
spark.sql("...").show(5)

3.Machine Learning and Advanced Analytics

一個Kmean的例子

//延續上面的數據,使用ML算法時先把DF中各數據類型轉換爲numerical values vector
staticDataFrame.printSchema()//staticDataFrame是用read獲取的DF,非readStream。此處先打印Schema,查看各列的數據類型,是否nullable等
val preppedDataFrame = staticDataFrame
  .na.fill(0)
  .withColumn("day_of_week", date_format($"InvoiceDate", "EEE"))//提取timestamp類的星期,具體查看java.text.SimpleDateFormat
  .coalesce(5)//5個partition,可設置是否shuffle

//分開training和test sets,下面採取手動方式,MLlib有其他APIs來實現。下面的劃分方式並非最優
val trainDataFrame = preppedDataFrame
  .where("InvoiceDate < '2011-07-01'")
val testDataFrame = preppedDataFrame
  .where("InvoiceDate >= '2011-07-01'")

//將day_of_week轉換爲相應的數量值,如Sun -> 7
val indexer = new StringIndexer()
  .setInputCol("day_of_week")
  .setOutputCol("day_of_week_index")
//僅僅用數值表示day_of_week是不合理的,要用OneHotEncoder進一步轉化
val encoder = new OneHotEncoder()
  .setInputCol("day_of_week_index")
  .setOutputCol("day_of_week_encoded")
//在使用任何ML算法前,要把col轉化爲vector(類似與python中pandas轉爲numpy)
val vectorAssembler = new VectorAssembler()
  .setInputCols(Array("UnitPrice", "Quantity", "day_of_week_encoded"))//python中,Array() -> []
  .setOutputCol("features")
//最後用pipeline把上面三個轉換連起來。這樣,每次day_of_week有更新,都可調用這個管道
val transformationPipeline = new Pipeline()
  .setStages(Array(indexer, encoder, vectorAssembler))

//確定數據並執行轉換
val fittedPipeline = transformationPipeline.fit(trainDataFrame)
val transformedTraining = fittedPipeline.transform(trainDataFrame).cache()//.cache將一份轉換後的dataset備份存到內存

//設定和訓練ML模型。術語:Algorithm指未訓練的模型,AlgorithmModel指訓練後的。Estimators可領流程更簡單,下面出於step by step展示
val kmeans = new KMeans()
  .setK(20)
  .setSeed(1L)
val kmModel = kmeans.fit(transformedTraining)

//把模型用到test set結果
val transformedTest = fittedPipeline.transform(testDataFrame)
kmModel.computeCost(transformedTest)

4.Lower-Level APIs

對於新版的Spark,用戶一般只會偶爾運用RDD,例如讀取和操作一些非常原始的數據。通常應該用結構化APIs,上面提到的。

//並行化生成RDD並轉化爲DF。scala和python中的RDD不一樣
spark.sparkContext.parallelize(Seq(1, 2, 3)).toDF()//python:parallelize([Row(1), Row(2), Row(3)])

Part II. Structured APIs—DataFrames, SQL, and Datasets


Structured API Overview

與底層RDD相比,結構化API有schema信息來提供更有效率的儲存(Tungsten)和處理(Catalyst),且Spark能夠inspect運算的邏輯意義。

結構化APIs可以操作各種數據,包括非結構化log files,半結構化 CSV和結構化的Parquet。這些APIs包括三部分:Datasets, DataFrames, SQL tables and views(這其實和DF一樣,用於區分SQL編程)。他們主要應用於batch和streaming計算。這兩種計算很容易通過結構化APIs轉換。

1.Spark Types

Spark通過Catalyst來維護它自身的類型,即Spark type不是其他編程語言中的某種類型,其他語言需要映射成Spark type才能執行。

創建某種類型:val b = ByteType (python要加上括號),具體各種數據類型的描述和創建看書中的表格或Spark文檔。

DataFrames and Datasets

"untyped" DataFrames 指DF在runtime才確定類型。
"typed" Datasets 則在complie time確定,這是相對於前者的優勢,另外就是可以保持數據類型。

簡單來說,在Java和Scala中有Datasets,它包含DF和其他非Row類Datasets;在非JVM語言中只有DF。例如:

在Scala中,spark.range(2).toDF().collect() 的range產生Datasets,python則直接生成DF

DF是由一系列行組成的,行的類型爲Row。這種Row類型是優化過的,避免JVM類型中的garbage-collection and object instantiation消耗。而列代表computation expression,它可被調用於每行。Schemas定義每列的名字和數據類型。

上述討論僅需知道:使用DF是在享受Spark的內部優化形式。所有的Spark language APIs(適用於Spark的語言)擁有相同的效率。

garbage-collection會佔用更多內存,其產生的序列化時間也會減慢計算

使用Tungstenl既可以 on-heap (in the JVM) 也可 off-heap storage。如果按後者儲存,要留足夠空間給 off-heap allocations,可通過UI查看。

2.Structured API Execution

  • 寫代碼(DF/Dataset/SQL)並提交
  • Spark得到unresolved logical plan(代碼合法但未判斷data是否存在,如某列、表)
  • 分析對比Catalog(保存數據信息)後得到 resolved logical plan
  • 通過Catalyst Optimizer邏輯優化得到optimized logical plan
  • 從optimized locical plan中得出不同的物理執行策略,利用Cost Model得出最優physical plan ,它包含一系列RDDs和transformation。(很像編譯器了)
  • 在集羣執行最優Physical Plan(執行過程通過生成本地Java字節碼去除整個tasks或者stages來進一步優化)

Basic Structured Operations

本節主要爲實踐。

1.Schemas

通常推斷獲取是可行的,但出於謹慎,應用於ETL時,最好通過手動設置Schemas。CSV和JSON等plain-text file 可能讀取有點慢,有時甚至完全推斷不出正確類型。

DF.schema可查看Schema類型,它是StructType包含多個StructField(列信息)

StructType(StructField(DEST_COUNTRY_NAME,StringType,true)...) 在python中多了一層List()來包含所有StructField

根據該結構,我們可以自定義Schema

val myManualSchema = StructType(Array(//Array() -> []
  StructField("DEST_COUNTRY_NAME", StringType, true),//Spark Type
  StructField("ORIGIN_COUNTRY_NAME", StringType, true),
  StructField("count", LongType, false,
    Metadata.fromJson("{\"hello\":\"world\"}"))//可設置元數據,python: metadata={"hello":"world"}
))
//可以用.printTreeString先檢查一下

val df = spark.read.format("json").schema(myManualSchema)
  .load("path") 

2.Columns and Expressions

在Spark裏,列是表達式,它代表一個基於per-record(即每行)計算的值。所以要得到具體值,我們需要row,而row存在於DF,所以col的內容操作必須在DF框架下。

創建和引用col

下面是最簡單的兩種方式

col("someColumnName")column("someColumnName")

顯示引用df.col("someColumnName")或不加參數

scala可用$"someColumnName"

表達式

它指一系列transformations,對象爲每個record裏的一個或多個值(map之類)。

通過expr創建,最簡單情況下,expr("someCol")相當於col("someCol")

expr可以通過邏輯樹對string形式的transformation和col引用等表達式進行分析。如expr("someCol - 5")相當於col("someCol") - 5expr("someCol") - 5。這也是爲什麼SQL和DF代碼會得到相同效果的原因。

3.Records and Rows

Row對象在內部表示爲字節數組。我們只用列表達式來操作他們。

創建和引用

val myRow = Row("Hello", null, 1, false)
//在Scala,use the helper methods or explicitly coerce the values。
myRow(0) // type Any,在Python中可以自動確定類型myRow[0],不需下面操作
myRow(0).asInstanceOf[String] // String
myRow.getString(0) // String
myRow.getInt(2) // Int

4.DataFrame Transformations

創建DF

// 最方便的方式
case class A (id: Int) // 要放到main函數外面
val df = Seq(new A(0)).toDF()

//創建schema
val myManualSchema =  StructType(Array(
   StructField("some", StringType, true),
   StructField("col", StringType, true),
   StructField("names", LongType, false)))//如果names爲null,會對創建後的DF進行操作時出錯(因爲lazy)
val myRows = Seq(Row("Hello", null, 1L))
val myRDD = spark.sparkContext.parallelize(myRows)
val myDf = spark.createDataFrame(myRDD, myManualSchema)//顯式創建,如果myRows是Seq(case class()),就可以不加schema或直接.toDF創建。
//上面三行用python只需兩行
myRow = Row("Hello", None, 1)
myDf = spark.createDataFrame([myRow], myManualSchema)

myDf.show()

//如果是text輸入的RDD
rdd.map(_split(" ")).map(line => caseclass(line(0).toInt, line(1)....)).toDF

rdd.map(_split(" ")).map(line => Row(line(0).toInt, line(1)....)).toDF //還要創建structType

//創建視圖,方便SQL
df.createOrReplaceTempView("dfTable") 

myDf.select("id", "v_2").as[(String, Int)]
.groupByKey{case (user, _) => user}
.flatMapGroups{case (a,b) => Some(a)}

sc.para
  .map(x => (x, x))
  .groupByKey( a => a._1)
  .map(a => a._1)
  .count()

非創建新DF操作

//選擇
//此處指用於理解,通常用selectExpr
df.select("DEST_COUNTRY_NAME", "ORIGIN_COUNTRY_NAME")//裏面的“name”和df.col("name"), col("name"),column("name"),'name, $"name", expr("name")是等價的。部分scala特色。注意Col對象和strings不能混用!!!
df.select(expr("DEST_COUNTRY_NAME AS destination").alias("A"))//將DEST_COUNTRY_NAME改名爲destination後並建立一個A的alias,即使最後顯示的列名爲A。
//用selectExpr更靈活,一個string一個col表達式,裏面還可寫非聚類的SQL。
df.selectExpr("DEST_COUNTRY_NAME as newColumnName", "DEST_COUNTRY_NAME")//選擇以新col name顯示的DEST_COUNTRY_NAME,和原名顯示的DEST_COUNTRY_NAME
df.selectExpr(
    "*", // include all original columns
    "(DEST_COUNTRY_NAME = ORIGIN_COUNTRY_NAME) as withinCountry")//顯示所有列以及一列數據類型爲boolean的withinCountry
df.selectExpr("avg(count)", "count(distinct(DEST_COUNTRY_NAME))")//對整份數據進行聚類計算
//literals適用於與某值或對象比較前的準備
df.select(expr("*"), lit(1).as("One"))//python只能alias。相當於創建了一列列名爲One,改列所有值爲1。實際上並沒有創建。

//重partition和合並,對經常filtered的col進行partition能提高效率。重partition會進行full shuffle,所以一般只當“當前partition小於未來partition數”或“想根據一組colspartition”時才用。
df.rdd.getNumPartitions
df.repartition(5)
df.repartition(col("DEST_COUNTRY_NAME"))
df.repartition(5, col("DEST_COUNTRY_NAME")).coalesce(2)//合併爲2個partition

//收集行到Driver,目前沒有特定的operation,但下面方法能達到效果。
df.first 
df.take(5) // selects the first N rows
df.show(5, false) // prints it out nicely。第二個參數是否truncate
df.collect()//collect所有!
df.toLocalIterator()//按partition提取數據as an iterator,可逐個partition地迭代整份數據

運用expr時,有dash或space的列名要通過(`)把名字擴起來轉義

創建新DF的操作

//添加列
df.withColumn("numberOne", lit(1))//lit是將Scala的int轉換爲Spark類型
df.withColumn("withinCountry", expr("ORIGIN_COUNTRY_NAME == DEST_COUNTRY_NAME"))

//改列名(兩種方式)
df.withColumn("Destination", expr("DEST_COUNTRY_NAME")).columns
df.withColumnRenamed("DEST_COUNTRY_NAME", "dest").columns

//改類型
df.withColumn("count2", col("count").cast("long"))

//過濾filter和where皆可
df.filter(!"count < 2").filter("...")//順序不重要
df.filter($"count" < 2)
df.filter(col("count") < 2)
df.filter(df("count") < 2)

//抽樣(良好的編程習慣)
val seed = 5
val withReplacement = false
val fraction = 0.5
df.sample(withReplacement, fraction, seed).count()

//隨機劃分(適合ML)
val dataFrames = df.randomSplit(Array(0.25, 0.75), seed)
dataFrames(0) // 第一份,python用[0]

//不能append(會改變DF),只能union(創建新DF,目前基於地址而非schema,所以union結果有可能與預料的不一樣)
df.union(newDF)
  .where("count = 1")//此處直接解釋SQL,下面則需要把"United States"變爲lit("United States")後比較
  .where($"ORIGIN_COUNTRY_NAME" =!= "United States")//scala在中Spark中用=!=表不等於,===表等於(上面==是在“”裏面的),也可用equalTo,not,leq等

//sort和orderBy,兩者一樣。有asc_nulls_first,sortWithinPartitions之類的。
df.orderBy(expr("count desc"))//也可orderBy($"count".desc)
df.orderBy(desc("count"), asc("DEST_COUNTRY_NAME"))

//其他簡單操作
df.drop("colName")
df.dropDuplicates
df.distinct()
df.limit(5) //其實當數據量大時,limit效率很低,因爲它會shuffle,且將所有符合條件的數據存到一個partition中。建議用rdd -> sortBy -> zipWithIndex -> filter(index < n) -> key

Spark默認不是case sensitive,可更改set spark.sql.caseSensitive true

String表達式的boolean運算符還可以是=、<>


Working with Different Types of Data

APIs資料(都在sql模塊)

DF方法:通過DataFrame 或 Dataset 類找。它有兩個模塊DataFrameStatFunctions(靜態方法)和DataFrameNaFunctions(處理null數據)

column方法

其他語言中的類型轉換爲Spark Typesdf.select(lit(5), lit("five"), lit(5.0))

1.Working with Booleans

//要將Boolean寫成鏈式,Spark會flatten所有filters成單獨statement並返回and的結果。思路是先弄幾個filters,然後在where或withColumn裏面用and或or把他們連在一起
val priceFilter = col("UnitPrice") > 600
val descripFilter = col("Description").contains("POSTAGE")
df.filter(col("StockCode").isin("DOT")).filter(priceFilter.or(descripFilter))
  .show()
//python中第二三行
descripFilter = instr(df.Description, "POSTAGE") >= 1
df.where(df.StockCode.isin("DOT")).where(priceFilter | descripFilter).show()

//也可以直接創建列
val DOTCodeFilter = col("StockCode") === "DOT"//上面第一個where的filter
df.withColumn("isExpensive", DOTCodeFilter.and(priceFilter.or(descripFilter)))
  .where("isExpensive")//返回該列爲true的rows
  .select("unitPrice", "isExpensive").show(5)

用eqNullSafe("hello")類方法更安全

2.Working with Numbers

//計算指數
val fabricatedQuantity = pow(col("Quantity") * col("UnitPrice"), 2) + 5
df.select(expr("CustomerId"), fabricatedQuantity.alias("realQuantity")).show(2)
//也可以直接在df.selectExpr()寫SQL
df.selectExpr(
  "CustomerId",
  "(POWER((Quantity * UnitPrice), 2.0) + 5) as realQuantity").show(2)

//round默認爲up,bround是HALF_EVEN
df.select(round(col("UnitPrice"), 1).alias("rounded"), col("UnitPrice"))//保留一位

//相關係數
df.stat.corr("Quantity", "UnitPrice")
df.select(corr("Quantity", "UnitPrice")).show()

//統計,如果代碼中需要describe中的數,用mean,stddev,max等來取值
df.describe().select().show()

//計算某分位的值
val colName = "UnitPrice"
val quantileProbs = Array(0.5)
val relError = 0.05//設置0的話耗費大
df.stat.approxQuantile("UnitPrice", quantileProbs, relError) 

//計算各個組合的頻率
df.stat.crosstab("StockCode", "Quantity").show()//交叉表顯示,所以僅限兩個col
df.stat.freqItems(Seq("StockCode", "Quantity")).show()

//添加id
df.select(monotonically_increasing_id()).show(2)

3.Working with Strings

//首字母大寫,其他lower,upper
df.select(initcap(col("Description")))

//trim,ltrim,rtrim
lpad(lit("HELLO"), 6, "a")//結果|aHELLO|,3時爲|HEL|,3位整體長度,HELLO長度大於3,會從右邊截掉部分

//正則表達
//replace
val simpleColors = Seq("black", "white", "red", "green", "blue")
val regexString = simpleColors.map(_.toUpperCase).mkString("|")
//python就直接寫“BLACK|WHITE|RED|GREEN|BLUE”
df.select(
  regexp_replace(col("Description"), regexString, "COLOR").alias("color_clean"),
  col("Description")).show(2)//把所有符合regexString的string變爲COLOR。對於“”的空白值(連空格都沒有的),要用null中的方法替換。
//字母水平的替代
df.select(translate(col("Description"), "LEET", "1337"), col("Description"))
//Description裏的L變爲1,E變爲3……

//提取第一個符合regex的
val regexString = simpleColors.map(_.toUpperCase).mkString("(", "|", ")")
//python就直接寫“(BLACK|WHITE|RED|GREEN|BLUE)”,下面就是替換成regexp_extract,並提取第幾個符合的
df.select(
     regexp_extract(col("Description"), regexString, 1).alias("color_clean"),
     col("Description")).show(2)

//含有單個
val containsBlack = col("Description").contains("BLACK")//python: containsBlack = instr(col("Description"), "BLACK") >= 1
//含有多個
val simpleColors = Seq("black", "white", "red", "green", "blue")
val selectedColumns = simpleColors.map(color => {
   col("Description").contains(color.toUpperCase).alias(s"is_$color")
}):+expr("*") // 不加:+expr("*")就不能用下面的:_*
df.select(selectedColumns:_*).where($"is_white" || $"is_red")
  .select("Description").show(3, false)
//python實現
simpleColors = ["black", "white", "red", "green", "blue"]
def color_locator(column, color_string):
  return locate(color_string.upper(), column)\
          .cast("boolean")\
          .alias("is_" + c)
selectedColumns = [color_locator(df.Description, c) for c in simpleColors]
selectedColumns.append(expr("*")) // has to a be Column type

df.select(*selectedColumns).where(expr("is_white OR is_red"))\
  .select("Description").show(3, False)

4.Working with Dates and Timestamps

兩種類型:datas日期,timestamps日期和時間

可在SQL裏設置時區spark.conf.sessionLocalTimeZone

TimestampType支持二級準度,如果使用毫秒或微秒,需要把他們作爲longs才能解決問題。

//下面得到一個10*3的表,第一列爲id
val dateDF = spark.range(10)
  .withColumn("today", current_date())
  .withColumn("now", current_timestamp())

|2018-08-24|2018-08-24 02:17:18.861|

//加減數值
dateDF.select(date_sub(col("today"), 5), date_add(col("today"), 5))
//日期差
dateDF.withColumn("week_ago", date_sub(col("today"), 7))
  .select(datediff(col("week_ago"), col("today"))).show(1)
dateDF.select(
    to_date(lit("2016-01-01")).alias("start"),//to_date的格式是根據java SimpleDateFormat的。spark不能轉化時用null填充,如yyyy-dd-mm格式不能轉
    to_date(lit("2017-05-22")).alias("end"))
  .select(months_between(col("start"), col("end"))).show(1)

//轉化爲date或datestamp(爲解決上面註釋的問題,在to_date中加上format),to_timestamp參數一樣
val dateFormat = "yyyy-dd-MM"
val cleanDateDF = spark.range(1).select(
    to_date(lit("2017-12-11"), dateFormat).alias("date"))
//比較
cleanDateDF.filter(col("date2") > lit("2017-12-12")).show()

5.Working with Nulls in Data

在Spark,缺失值用null比空白要好。DF中的schema表示not nullable時,spark依然會讓null進入?

//Coalesce得到第一個沒有null的列,如果“Description”某row爲null,該null會被下一列“CustomerId”的同一row位置的非null值替代
df.select(coalesce(col("Description"), col("CustomerId"))).show()
//其他null情況
ifnull(null, 'return_value')
nullif('value', 'value')//相等時null,不等於時返回第二個value
nvl(null, 'return_value')
nvl2('not_null', 'return_value', "else_value")

//drop null
df.na.drop("any", Seq("StockCode", "InvoiceNo"))//默認any,是null就刪。all是當所有都爲null時刪

//fill null(裏面可以放map)
df.na.fill(5, Seq("StockCode", "InvoiceNo"))//用5來填充
val fillColValues = Map("StockCode" -> 5, "Description" -> "No Value")
df.na.fill(fillColValues)//python用自己的map形式

其他操作

//replace
df.na.replace("Description", Map("" -> "UNKNOWN"))
//python如下
df.na.replace([""], ["UNKNOWN"], "Description")

6.Working with Complex Types(structs, arrays and maps)

Struct相當於一組col

//創建
//方式1
df.selectExpr("(Description, InvoiceNo) as complex", "*")
//方式2
df.selectExpr("struct(Description, InvoiceNo) as complex", "*")
//方式3
val complexDF = df.select(struct("Description", "InvoiceNo").alias("complex"))
//創建Struct後可以通過dot或者getField來引用
complexDF.select("complex.Description")//可以.*,相當於把struct拆分回來
complexDF.select(col("complex").getField("Description"))

Arrays

//split
df.select(split(col("Description"), " ")//產生array
  .alias("array_col"))
  .selectExpr("array_col[0]")//可用類似python的語法提取第一個元素
//length
size(Array)//col裏面的數據類型是Array
//array_contains
array_contains(Array, "A")//Array裏是否有A
//explode,參數是數據類型爲Array的col,爲參數列裏面的Array裏每一個元素創建一行數據,除該元素外,其他列都是原元素所處行的其他列的複製。
df.withColumn("splitted", split(col("Description"), " "))
  .withColumn("exploded", explode(col("splitted")))
  .select("Description", "InvoiceNo", "exploded").show(2)

Map

df.select(map(col("Description"), col("InvoiceNo")).alias("complex_map"))//Description列作key,另一個作value
  .selectExpr("complex_map['WHITE METAL LANTERN']")//提取值,key值不匹配的row顯示null

//對map用explode可將它們轉換回cols

7.Working with JSON

//創建
val jsonDF = spark.range(1).selectExpr("""
  '{"myJSONKey" : {"myJSONValue" : [1, 2, 3]}}' as jsonString""")

jsonDF.select(
    get_json_object(col("jsonString"), "$.myJSONKey.myJSONValue[1]") as "column"// 2,
    json_tuple(col("jsonString"), "myJSONKey")//{"myJSONValue" : [1, 2, 3]}
).show(2)

//to_json參數爲StructType或Map,from_json操作相反,不過要創建schama
val parseSchema = StructType(Array(
  StructField("InvoiceNo",StringType,true),
  StructField("Description",StringType,true)))
df.selectExpr("(InvoiceNo, Description) as myStruct")
  .select(to_json(col("myStruct")).alias("newJSON"))
  .select(from_json(col("newJSON"), parseSchema), col("newJSON")).show(2)

8.User-Defined Functions

定義的函數會被序列化後發送到所有executors。用Scala或Java寫的function除了發送外沒有其他額外消耗。但是python會有,Spark需要在worker節點上啓動python進程,將數據轉化爲該進程理解的形式,得出結果後還要轉換回來。

//從創建到使用
val udfExampleDF = spark.range(5).toDF("num")
def power3(number:Double):Double = number * number * number
val power3udf = udf(power3(_:Double):Double)
udfExampleDF.select(power3udf(col("num")))

//登記後,可由DF函數變爲Spark SQL函數,且可以跨語言使用。但還不能用在string表達式
spark.udf.register("power3", power3(_:Double):Double)//python要多加DoubleType()的類型參數
udfExampleDF.selectExpr("power3(num)").show(2)

通過Hive寫的UDF或SQL,在創建SparkSession要加上.enableHiveSupport(),這隻支持預編譯的Scala和Java包,需要加依賴(Maven就spark-hive_2.11之類的)。下面TEMPORARY可刪,如果想登記爲永久function在Hive Metastore上

CREATE TEMPORARY FUNCTION myFunc AS 'com.organization.hive.udf.FunctionName

Aggregations

聚類需要key或grouping和一個聚類函數。該函數需要每個group產生一個結果。

groupings形式:整個DF(如select statement)、group by(n鍵,n聚類函數)、window(和group by 一樣,但rows分批)、grouping set(即多層group by,SQL原生,DF則通過rollup和cube)

grouping產生RelationalGroupedDataset類

  • 下面會有一些approximation functions,畢竟大量數據聚合會很費時間,使用這些函數也有利於交互和臨時分析
  • 加載大量小文件(總量也小)可以在.load後.coalesce減少partition並cache

1.Aggregation Functions

基本都在org.apache.spark.sql.functions

//count
df.count()//這個action既可瞭解數量,也可觸發cache。另外要注意,在使用count(*)時,即使某行全是null,仍會計算。而單獨列的count會忽略null,即df.select(count(col("xxx")))

//countDistinct
df.select(countDistinct("StockCode")).show() # 4070
approx_count_distinct("StockCode", 0.1) # 3364

//各種一看就懂的函數
first, last, min, max, sum, sumDistinct
var_pop, var_samp, stddev_pop
skewness, kurtosis//偏斜和峯度
corr, covar_pop, covar_samp//當然,這需要兩個列

//平時計算完習慣用alias,下面有很多靈活的實現方式
df.select(
    count("Quantity").alias("total_transactions"),
    sum("Quantity").alias("total_purchases"),
    avg("Quantity").alias("avg_purchases"),
    expr("mean(Quantity)").alias("mean_purchases"))
  .selectExpr(
    "total_purchases/total_transactions",
    "avg_purchases",
    "mean_purchases").show()

//聚合爲複合類型
df.agg(collect_set("Country"), collect_list("Country")).show()

2.Grouping

//grouping with count
df.groupBy("InvoiceNo", "CustomerId").count().show()

//在groupby的基礎上進行多個聚類計算
df.groupBy("InvoiceNo").agg(//類似select,但只放聚合方法
  count("Quantity").alias("quan"),//count既是method又是expression,但我們很少把count作爲後者使用,如下一行。
  expr("count(Quantity)")).show()

//grouping with Maps
df.groupBy("InvoiceNo").agg("Quantity"->"avg", "Quantity"->"stddev_pop")

3.Window Functions

支持三類函數:ranking, analytic and aggregate

分組聚合,與其他組無關

val dfWithDate = df.withColumn("date", to_date(col("InvoiceDate"),
  "MM/d/yyyy H:mm"))

val windowSpec = Window
  .partitionBy("CustomerId", "date")//將Id和date相同的分爲一組。如果不設置,最後只有一個partition
  .orderBy(col("Quantity").desc)//Quantity越大,下面的rank排名越低
  .rowsBetween(Window.unboundedPreceding, Window.currentRow)//all previous rows up to the current row,也有rangeBetween,即按隔多少個值來劃分,如rangeBetween(Window.currentRow, 1)中,下面不管有多少個1和2,1的統計值都會是1和2所有值的總和
+---+--------+---+
| id|category|sum|
+---+--------+---+
|  1|       a|  7|
|  1|       a|  7|
|  1|       a|  7|
|  2|       a|  4|
|  2|       a|  4|
+---+--------+---+

//直接用,取前n,會根據n的大小進行優化,不去全排。但window幾乎沒有mapside聚合。
df.withColumn("sum", sum(col("Quantity")).over(windowSpec))
.filter($"sum" <= n)

//聚合方法.over產生expression/col,可用於select
val maxPurchaseQuantity = max(col("Quantity")).over(windowSpec)
val purchaseDenseRank = dense_rank().over(windowSpec)// 排名沒有gap,和下面一樣,window只能用.unboundedPreceding和.currentRow。也有percentage rank
val purchaseRank = rank().over(windowSpec)

// 時間差
val df = spark.sparkContext.parallelize(Seq(
    (134, 30, "2016-07-02 12:01:40"),
    (134, 32, "2016-07-02 12:21:23"),
    (125, 30, "2016-07-02 13:22:56"),
    (125, 32, "2016-07-02 13:27:07")
  )).toDF("itemid", "eventid", "timestamp")
.withColumn("timestamp", col("timestamp").cast("timestamp"))
val w = Window.partitionBy("itemid").orderBy("timestamp")
val diff = col("timestamp").cast("long") - lag("timestamp", 1).over(w).cast("long")
df.withColumn("diff", diff).show(false)

fWithDate.where("CustomerId IS NOT NULL").orderBy("CustomerId")
  .select(
    col("CustomerId"),
    col("date"),
    col("Quantity"),
    purchaseRank.alias("quantityRank"),
    purchaseDenseRank.alias("quantityDenseRank"),
    maxPurchaseQuantity.alias("maxPurchaseQuantity")).show()

4.Grouping Sets

跨組聚合。操作前先刪除null值!

//rollup,下面方法其實直接用SQL更方便。結果中的null表示總計。下面代碼的結果包括:
//(1)所有日期和國家的Quantity總數
//(2)每個日期所有國家的Quantity總數
//(3)每個國家在每個日期的Quantity總數
val rolledUpDF = dfNoNull.rollup("Date", "Country").agg(sum("Quantity"))
  .selectExpr("Date", "Country", "`sum(Quantity)` as total_quantity")
  .orderBy("Date")

//cube是更深層的聚合。下面代碼的結果比上面多了:
//(4)每個國家所有日期的Quantity總數
dfNoNull.cube("Date", "Country").agg(sum(col("Quantity")))
  .select("Date", "Country", "sum(Quantity)").orderBy("Date").show()

//grouping_id可以提取cube的某個層次。下面代碼中,id數字對應的層次:3-(1),2-(2), 1-(4), 0-(3)
dfNoNull.cube("customerId", "stockCode").agg(grouping_id(), sum("Quantity"))
.orderBy(expr("grouping_id()").desc)
.filter($"grouping_id()" === 0)
.show()

//Pivot透視表
val pivoted = dfWithDate.groupBy("date").pivot("Country").sum()

5.UDAFs

自定義聚類函數,麻煩,但比ds的mapGroups和rdd的aggregateByKey高效。

注意,雖然buffer允許array,但array作爲buffer時,每次update都要掃描整行row。

merge次數取決於key在各partition的分佈以及partition數量,即如果每個key在各個partition都至少出現一次(最差情況),merge次數爲partition數 x key數

//先要創建類
class BoolAnd extends UserDefinedAggregateFunction {
  def inputSchema: org.apache.spark.sql.types.StructType =
    StructType(StructField("value", BooleanType) :: Nil)//加Nil轉換爲List包裹的StructField
  
  //一個容器,存放計算時臨時產生的結果
  def bufferSchema: StructType = StructType(
    StructField("result", BooleanType) :: Nil
  )
  
  //返回類型
  def dataType: DataType = BooleanType
  
  //相同輸入是否返回相同結果
  def deterministic: Boolean = true
  
  //初始化上面bufferSchema設定的內容
  def initialize(buffer: MutableAggregationBuffer): Unit = {
    buffer(0) = true
  }
  
  //根據當前row如何更新buffer。對於兩個arg,要通過.getAs[T](index or colName)來得相應的值  
  def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
    buffer(0) = buffer.getAs[Boolean](0) && input.getAs[Boolean](0)
  }
  def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
    buffer1(0) = buffer1.getAs[Boolean](0) && buffer2.getAs[Boolean](0)
  }
  
  //結果
  def evaluate(buffer: Row): Any = {
    buffer(0)
  }
}

//使用
val ba = new BoolAnd
spark.udf.register("booland", ba)//登記名字
import org.apache.spark.sql.functions._
spark.range(1)
  .selectExpr("explode(array(TRUE, TRUE, TRUE)) as t")
  .selectExpr("explode(array(TRUE, FALSE, TRUE)) as f", "t")
  .select(ba(col("t")), expr("booland(f)"))
  .show()

Join

1.實踐

//至少要有joinExpression,joinType可選
var joinType = "inner"//默認inner,所以這裏是多餘的
val joinExpression = person.col("graduate_program") === graduateProgram.col("id")
//然後才能join
person.join(graduateProgram, joinExpression, joinType).show()

//self joins
df1.join(df1,"id").show()

joinType: outer, left_outer, right_outer, left_semi(這個本質上不是join,而是filter。對應的右側數據存在,那左側rows保留), left_anti(存在則去掉),cross笛卡爾(最危險)。用cross時,對每個DF進行factors.mapPartitions(_.grouped(blockSize)),然後調用crossJoin能夠提高效率。

如果key1在兩個表不是唯一的,那麼innerjoin也是會crossjoin的。即表1有2個key1,表2有2個key1,則innerjoin後會有2 x 2行

//複雜類型join,joinExpression的邏輯可能是一個表的一個row和另一個表的所有row比較,只要返回true就連在一起。所以只要joinExpression包含兩表的信息,且返回boolean即可,這樣能夠實現很多複雜的join判斷
//下面的join的判斷爲person表中id列的值是否存在於sparkStatus表中的spark_status數組中
person.withColumnRenamed("id", "personId")
  .join(sparkStatus, expr("array_contains(spark_status, id)")).show()
+---------------+---+
|   spark_status| id|
+---------------+---+
|          [100]|100|
|[500, 250, 100]|500|
|[500, 250, 100]|250|
|[500, 250, 100]|100|
|     [250, 100]|250|
|     [250, 100]|100|
+---------------+---+
//重名列。假設person和gradPrograDupe有同名列graduate_program。此時join後選擇同名列自然會出錯,可在select(person.col("graduate_program"))來指明
//下面join時去掉右邊組的同名列
person.join(gradProgramDupe,"graduate_program").select("graduate_program").show()

2.Spark如何實現Joins

node-to-node communication strategy 和 per node computation strategy 對應下面兩種方式

shuffle join和broadcast join。前者的情況是,兩個表都很大,不能單獨存到一個worker節點上,並有剩餘空間存另一個表的一部分,這時worker就必須communicate了。後者情況是,如果其中一個表足夠小,可以把該表廣播到各個worker節點上,廣播後就不需交流了。當然,廣播的過程也是有消耗的。

當兩個表都很小時,最好讓Spark自己決定。如果發現什麼奇怪情況,也可以通過下面方式設置。

person.join(broadcast(graduateProgram), joinExpr).explain()//可查看計劃,看Spark採取哪種策略。這裏已經設置了broadcast了

Data Sources

六大核心數據源CSV, JSON, Parquet, ORC, JSBC/ODBC connections, Plain-text files,更多去spark-packages.org下載。

將不同數據源,經過混合處理,寫到特定的系統上去

1.Reader and Writer

Read API

spark.read.format(...).option("key", "value").schema(...).load()

format(Parquet默認),option和schema都是可選,不會改變reader類型

option("mode", "x")中x可填下面三種:

  • permissive默認,遇到損壞的records時將所有字段設置爲null,並將所有損壞的記錄放在名爲“_corrupt_record”的字符串列中
  • dropMalformed刪除有問題的records
  • failFast有問題直接fail

由於lazy的原因,即便是找不到文件的問題,也要等Spark真正action纔會發現

Write API

df.write.format(...).option(...).partitionBy(...).bucketBy(...).sortBy(...).save(folder)

format(parquet默認)

mode有append,overwrite,errorIfExists(默認),ignore

2.各種數據格式的介紹

CSV Files

選項(只是常用)sep, header, escape(轉義符), inferSchema, ignore(Leading/Trail)WhiteSpace, nullValue(null值的形式), nanValue, positiveInf(正無窮), negativeInf, compression or codec, dateFormat, timestampFormat, maxColumns(20480), maxCharsPerColumn, escapeQuotes(是否轉義), maxMalformedLogPerPartition(錯誤記錄長度10), quoteAll, multiLine

JSON Files

在Spark, 一般指的是單行分隔JSON。(可以通過multiLine設定多行)

選項compression or codec, dateFormat, timestampFormat, primitiveAsString, allowComments, allowUnquotedFieldNames, allowSingleQuotes, allowNumbericLeadingZeros, allowBackslashEscapingAnyCharacter, columnNameOfCorruptRecord, multiLine

Parquet Files

column-oriented,可以提取特定列。支持複雜類型(即array等)。讀取方便,file本身有schema。

舊版本Spark寫出的Parquet和新版的兼容不太好?

選項compression.codec, mergeSchema

ORC Files

和Parquet很相似,前者更適合Hive,後者更適合Spark。

沒有選項。

讀取hive table時,spark會改變元數據並cache結果。如果元數據被改變了,可以用sqlContext.refreshTable("tablename")來更新。可以取消cache: spark.sql.parquet.cacheMetadata = false

SQL Databases

選項有很多,如numPartitions,此處略過。

//讀寫數據庫需要兩樣東西JDBC driver和數據源
//測試鏈接
val connection = DriverManager.getConnection(url)
connection.isClosed()
connection.close()

val dbDataFrame = spark.read.format("jdbc").option("url", url)
  .option("dbtable", tablename).option("driver",  driver).load()//書中有PostgreSQL的讀取

Spark在對數據庫數據進行操作時,會用各種filter以提高load效率。比如查詢某列,Spark只會load該列;如果用filter,spark會直接在數據庫filter再load。

但通常的做法是先用SQL查詢,然後Spark讀取該查詢的數據。例如想下面寫一個SQL查詢,然後放到.option("dbtable", pushdownQuery)

val pushdownQuery = """(SELECT DISTINCT(DEST_COUNTRY_NAME) FROM flight_info)
  AS sub_flight_info""” 

可以將一份數據放到不同的partitions,也可多份一partition。.option("numPartitions", 10)

val props = new java.util.Properties
props.setProperty("driver", "org.sqlite.JDBC")
val predicates = Array(
  "DEST_COUNTRY_NAME = 'Sweden' OR ORIGIN_COUNTRY_NAME = 'Sweden'",
  "DEST_COUNTRY_NAME = 'Anguilla' OR ORIGIN_COUNTRY_NAME = 'Anguilla'")//這兩個結果是不相交的,如果相交,會有重複項出現
spark.read.jdbc(url, tablename, predicates, props).rdd.getNumPartitions // 另一種讀取數據庫的方法。

可以設定讀取窗口

//下面根據tablename中的一個列colName進行partition,並設置了窗口的最值。數據會被平均分到numPartitions個partition裏
spark.read.jdbc(url,tablename,colName,lowerBound,upperBound,numPartitions,props)

寫入數據庫

val newPath = "jdbc:sqlite://tmp/my-sqlite.db"
csvFile.write.mode("overwrite").jdbc(newPath, tablename, props)

Text Files

讀寫

spark.read.textFile("/data/flight-data/csv/2010-summary.csv")
  .selectExpr("split(value, ',') as rows").show()
//寫的時候要保證只有一col的string,而非partition寫,但partition寫就不會是單個文件。
csvFile.select("DEST_COUNTRY_NAME").write.text("path/file")

3.Advanced I/O Concepts

可分隔的文件類和壓縮

把文件複製多份存到HDFS中,可以提高I/O效率(並行讀寫)。但同時要管理好壓縮。本書推薦Parquet和gzip。

partition輸出(Bucketing可能更好)

csvFile.write.mode("overwrite").partitionBy("DEST_COUNTRY_NAME").save()

這樣每個country都有一個folder。當用戶經常filter某個對象,可以用這種方式輸出

Bucketing

控制數據寫到制定的每個file,這樣在讀取,join或agg時,就可以避免一些shuffle。

csvFile.write.format("parquet").mode("overwrite")
  .bucketBy(numberBuckets, columnToBucketBy).saveAsTable("bucketedFiles")

控制文件輸出大小

在option裏設置maxRecordsPerFile


Spark SQL

連接Hive metastore有幾個步驟:設定spark.sql.hive.metastore.version。如果要改變HiveMetastoreClient的初始化,還要設置spark.sql.hive.metastore.jars。合適的類前綴spark.sql.hive.metastore.sharedPrefixes

1.運行Spark SQL 查詢的三種方式

設置服務器

通過Spark SQL CLI實現,但它不能與Thrift JDBC server通信。./bin/spark-sql

配置Hive在conf中的三個文件hive-site.xml, core-site.xml, and hdfs-site.xml。完整的option查spark-sql --help

通過語言接口

SQL和DF轉換:load後.createOrReplaceTempView("tableName")就得到SQL所用的table。然後通過SparkSession.sql("")便可寫sql語法查詢該表,它返回DF。這個方法很有用,因爲有些轉換代碼在SQL中比在DF中更容易寫。

SparkSQL Thrift JDBC/ODBC Server

一些軟件,如Tableau可通過Java Database Connectivity (JDBC) interface 連接Spark driver去執行Spark SQL查詢。通過./sbin/start-thriftserver.sh啓動服務,該腳本接受所有spark-submit命令行選項,例如--master local[2] --jars xx/mysql-connector-java-xxx.jar。服務默認在localhost:10000,可通過環境變量或系統特徵來修改,如--hiveconf hive.server2.thrift.port=xxxx。之後通過beeline測試連接,即beeline -u jdbc:hive2:localhost:10000 -n hadoop其中n是用戶名。

這種方式不管有多少個客戶端,都是一個spark application,使得多個客戶端共享數據。

2.Catalog

Spark SQL的最頂層抽象,它關係到元數據、databases、table、function和views。

table

它在邏輯上等價於DF,可以對它進行前幾節提到的操作,不同在於後者定義在編程語言中,前者定義在database中。這意味着創建一個table就是一個default database。

在2.X中沒有temporary table,只有views(不存數據),所有tables都會有數據。這意味着drop a table會丟失數據。

managed 和 unmanaged tables,前者爲通過saveAsTable實現的table(Spark記錄所有相關信息),後者爲其他方法定義的table。

創建和插入tables

語法和之前的I/O類似

COMMENT "managed table"
CREATE TABLE flights_csv (
  DEST_COUNTRY_NAME STRING,
  ORIGIN_COUNTRY_NAME STRING COMMENT "remember, the US will be most prevalent",
  count LONG)
USING csv OPTIONS (header true, path '/data/flight-data/csv/2015-summary.csv')

COMMENT "也可以通過query創建。加上IF NOT EXISTS更好,如果去掉USING parquet,就會默認創建Hive兼容表"
CREATE TABLE IF NOT EXISTS flights_from_select USING parquet PARTITIONED BY (DEST_COUNTRY_NAME) AS SELECT * FROM flights LIMIT 5

COMMENT "unmanaged"
CREATE EXTERNAL TABLE hive_flights_2
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
LOCATION '/data/flight-data-hive/' AS SELECT * FROM flights

COMMENT "插入,Partition可選"
INSERT INTO partitioned_flights
  PARTITION (DEST_COUNTRY_NAME="UNITED STATES")
  SELECT count, ORIGIN_COUNTRY_NAME FROM flights
  WHERE DEST_COUNTRY_NAME='UNITED STATES' LIMIT 12

元數據

DESCRIBE TABLE flights_csv # 查看comment信息
SHOW PARTITIONS partitioned_flights # 查看partitioned table的信息

# REFRESH TABLE刷新與表關聯的所有緩存條目(實質上是文件)。 如果該表先前已被緩存,則下次掃描時它會lazily緩存
REFRESH table partitioned_flights
# repair主要是收集新partition信息
MSCK REPAIR TABLE partitioned_flights

drop tables(數據會丟失,謹慎)

#不能刪,只能drop
DROP TABLE IF EXISTS flights_csv;

如果drop的是unmanaged table,數據不會丟失,只是不能引用該table name

可以CACHE或UNCACHE TABLE

View

# 創建。在VIEW前可加TEMP,創建僅在當前會話期間可用但未註冊到數據庫的臨時視圖。還可以再加GLOBAL,這樣可以在整個Spark app中可見,但會在會話結束時被刪除。有
CREATE OR REPLACE GLOBAL VIEW just_usa_view AS
  SELECT * FROM flights WHERE dest_country_name = 'United States'

# Drop
DROP VIEW IF EXISTS just_usa_view;

Databeases

組織tables的工具。

SHOW DATABASES
CREATE DATABASE some_db
USE some_db #USE default直接到默認
SHOW tables IN databaseName # IN可選
SELECT * FROM default.flights #可以查詢其他庫的表
SELECT current_database() #查看當前數據庫
DROP DATABASE IF EXISTS some_db;

3.Advanced Topics

複雜類型(Structs, Lists, Maps)

# Structs
CREATE VIEW IF NOT EXISTS nested_data AS
    SELECT (DEST_COUNTRY_NAME, ORIGIN_COUNTRY_NAME) as country, count FROM flights
    
SELECT country.DEST_COUNTRY_NAME, count FROM nested_data

# List,分collect_list and collect_set
# 下面代碼由於groupby,所以同組的數據會放到一個分collect_list或分collect_set裏。可用[0]提取第一個數
SELECT DEST_COUNTRY_NAME as new_name, collect_list(count) as flight_counts,
  collect_set(ORIGIN_COUNTRY_NAME) as origin_set
FROM flights GROUP BY DEST_COUNTRY_NAME
# 下面會給每個row添加一列,該列每個元素爲ARRAY(1, 2, 3) 
SELECT DEST_COUNTRY_NAME, ARRAY(1, 2, 3) FROM flights
# 同樣可以用explode拆開
SELECT explode(collected_counts), DEST_COUNTRY_NAME FROM flights_agg

SQL Functions

SHOW FUNCTIONS "s*";
SHOW USER FUNCTIONS
SHOW SYSTEM FUNCTIONS
DESCRIBE #查functions描述

Subqueries

#Uncorrelated predicate subqueries
SELECT * FROM flights
WHERE origin_country_name IN (SELECT dest_country_name FROM flights
      GROUP BY dest_country_name ORDER BY sum(count) DESC LIMIT 5)

#Correlated predicate subqueries 下面查詢有往返的航線
SELECT * FROM aa f1
WHERE EXISTS (SELECT 1 FROM aa f2
            WHERE f1.dest_country_name = f2.origin_country_name)
AND EXISTS (SELECT 1 FROM aa f2
            WHERE f2.dest_country_name = f1.origin_country_name)

#Uncorrelated scalar queries 添加輔助信息,下面就是添加一列maximum
SELECT *, (SELECT max(count) FROM flights) AS maximum FROM flights

4.Miscellaneous Features

配置SQL

SET
spark.sql.inMemoryColumnarStorage.compressed //true
spark.sql.inMemoryColumnarStorage.batchSize //10000
spark.sql.files.maxPartitionBytes //128MB
spark.sql.files.openCostInBytes //4MB高估比較好
spark.sql.broadcastTimeout // 300s,超時就不再broadcast了
spark.sql.autoBroadcastJoinThreshold //10M,文件小於多少會自動廣播到所有workers
spark.sql.shuffle.partitions//200

Datasets

編碼器指示Spark生成代碼去序列化T對象。當使用DF或標準Structured APIs時,二進制結構會變成Row類型。如果用Dataset API的話,二進制結構就會變回T。當Row轉化爲T類時,會影響效率,但並沒有python用UDF時影響大。

使用Datasets一般有三個理由:DF操作無法滿足, 需要type-safety,某些情況比較方便。

Datasets的一些代碼,主要是展示如何使用

//創建,你定義schema
//Java
public class Flight implements Serializable{
  String DEST_COUNTRY_NAME;
  String ORIGIN_COUNTRY_NAME;
  Long DEST_COUNTRY_NAME;
}

Dataset<Flight> flights = spark.read
  .parquet("path")
  .as(Encoders.bean(Flight.class));

//Scala要創建的是單例類,該類的特徵爲:immutable,模式匹配時可解構,基於值比較而非引用。
case class Flight(DEST_COUNTRY_NAME: String,
                  ORIGIN_COUNTRY_NAME: String, count: BigInt)
val flightsDF = spark.read.parquet("path")
val flights = flightsDF.as[Flight]
//也可以直接對對象創建dataset,下面pandaPlace是一個case class的實例。也可以直接toDS()
df = spark.createDataFrame(Seq(pandaPlace))
//多層次schema,StructType構造器有很多,加上Array來具體確定其中一個;ArrayType(Type, Boolean),其中Boolean默認true
val pandasType = ArrayType(StructType(Array(
    StructField("id",LongType,false),
    StructField("zip",StringType,true),
    StructField("pt",StringType,true),
    StructField("happy",BooleanType,false),
    StructField("attributes",ArrayType(DoubleType,false),true))),
  true)
val tschema = StructType(Array(
  StructField("name",StringType,true),
  StructField("pandas",pandasType,true)))

//action現在通過action方法返回某個record就能直接通過.valueName來get值
flights.first.DEST_COUNTRY_NAME

//transformation,SQL有的就不要自定義,麻煩且效率會降低。下面不是UDF,是泛型函數
//filter
def originIsDestination(flight_row: Flight): Boolean = {
  return flight_row.ORIGIN_COUNTRY_NAME == flight_row.DEST_COUNTRY_NAME
}
flights.filter(flight_row => originIsDestination(flight_row))
//mapping
val destinations = flights.map(f => f.DEST_COUNTRY_NAME)

//Joins
case class FlightMetadata(count: BigInt, randomData: BigInt)

val flightsMeta = spark.range(500).map(x => (x, scala.util.Random.nextLong))//自動變爲兩列
  .withColumnRenamed("_1", "count").withColumnRenamed("_2", "randomData")
  .as[FlightMetadata]
//下面join的結果是兩個cols,裏面各自每row存放一個相應的類實例
val flights2 = flights
  .joinWith(flightsMeta, flights.col("count") === flightsMeta.col("count"))
//提取某列的值
flights2.selectExpr("_1.DEST_COUNTRY_NAME")

//如果直接用join,就會拆開原來的type,每個變量一列,合併成一個Row類DF。下面toDF可加可不加,指用於表示DF可與Dataset合併
val flights2 = flights.join(flightsMeta.toDF(), Seq("count"))

//grouping和aggregations
//groupBy,rollup和cube都可用,但會返回DF(即失去所定義類型的信息)
//如果想保留信息,用groupByKey
flights.groupByKey(x => x.DEST_COUNTRY_NAME).count()//結果是Dataset[]裏面的key是原類型變量的類型string
//其他聚類用agg加聚類函數加as把col轉化爲TypedColumn
ds.groupByKey(row => row.ID).agg(max("V2").as[Int]).show

flights.groupBy("DEST_COUNTRY_NAME").count()//結果是DF,裏面的類型是Spark Tpye
//groupByKey後,我們得到KeyValueGroupedDataset,即(key,原對象),可根據它可調用的方法自定義函數。如果調用flatMapGroups的話,形式如下,其中countryName是key,Flight是各組中的對象的類
def grpSum(countryName:String, values: Iterator[Flight]) = {
  values.dropWhile(_.count < 5).map(x => (countryName, x))//並沒有任何合併
}

flights.groupByKey(x => x.DEST_COUNTRY_NAME).flatMapGroups(grpSum).show(2)
+--------+---------------------------+
|_1      |_2                         |
+--------+---------------------------+
|Anguilla|[Anguilla,United States,21]|
|Paraguay|[Paraguay,United States,90]|
+--------+---------------------------+
//mapGroups
ds.groupByKey(row => row.ID).mapGroups{ case (g, iter) => (g, iter.map(_.V3).reduce(_+_))}.show

//調用mapValues
def grpSum2(f:Flight):Integer = {
  1
}
flights.groupByKey(x => x.DEST_COUNTRY_NAME).mapValues(grpSum2).count().take(5)
//調用reduceGroups
def sum2(left:Flight, right:Flight) = {
  Flight(left.DEST_COUNTRY_NAME, null, left.count + right.count)
}
flights.groupByKey(x => x.DEST_COUNTRY_NAME).reduceGroups((l, r) => sum2(l, r))
  .take(5)

 轉:https://www.cnblogs.com/code2one/p/9872010.html

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