RDD vs DataFrames vs DataSet
在SparkSQL中Spark爲我們提供了兩個新的抽象,分別是DataFrame和DataSet。他們和RDD有什麼區別呢?首先從版本的產生上來看:
RDD (Spark1.0) —> Dataframe(Spark1.3) —> Dataset(Spark1.6)
如果同樣的數據都給到這三個數據結構,他們分別計算之後,都會給出相同的結果。不同是的他們的執行效率和執行方式。
在後期的Spark版本中,DataSet會逐步取代RDD和DataFrame成爲唯一的API接口。
RDD
RDD是一個懶執行的不可變的可以支持Lambda表達式的並行數據集合。
RDD的最大好處就是簡單,API的人性化程度很高。
RDD的劣勢是性能限制,它是一個JVM駐內存對象,這也就決定了存在GC的限制和數據增加時Java序列化成本的升高。
Dataframe
與RDD類似,DataFrame也是一個分佈式數據容器。然而DataFrame更像傳統數據庫的二維表格,除了數據以外,還記錄數據的結構信息,即schema。同時,與Hive類似,DataFrame也支持嵌套數據類型(struct、array和map)。從API易用性的角度上看,DataFrame API提供的是一套高層的關係操作,比函數式的RDD API要更加友好,門檻更低。由於與R和Pandas的DataFrame類似,Spark DataFrame很好地繼承了傳統單機數據分析的開發體驗。
上圖直觀地體現了DataFrame和RDD的區別。左側的RDD[Person]雖然以Person爲類型參數,但Spark框架本身不瞭解Person類的內部結構。而右側的DataFrame卻提供了詳細的結構信息,使得Spark SQL可以清楚地知道該數據集中包含哪些列,每列的名稱和類型各是什麼。DataFrame多了數據的結構信息,即schema。RDD是分佈式的Java對象的集合。DataFrame是分佈式的Row對象的集合。DataFrame除了提供了比RDD更豐富的算子以外,更重要的特點是提升執行效率、減少數據讀取以及執行計劃的優化,比如filter下推、裁剪等。
DataFrame是爲數據提供了Schema的視圖。可以把它當做數據庫中的一張表來對待
DataFrame也是懶執行的。
性能上比RDD要高,主要有兩方面原因:
定製化內存管理
數據以二進制的方式存在於非堆內存,節省了大量空間之外,還擺脫了GC的限制
優化的執行計劃
查詢計劃通過Spark catalyst optimiser進行優化.
爲了說明查詢優化,我們來看上圖展示的人口數據分析的示例。圖中構造了兩個DataFrame,將它們join之後又做了一次filter操作。如果原封不動地執行這個執行計劃,最終的執行效率是不高的。因爲join是一個代價較大的操作,也可能會產生一個較大的數據集。如果我們能將filter下推到 join下方,先對DataFrame進行過濾,再join過濾後的較小的結果集,便可以有效縮短執行時間。而Spark SQL的查詢優化器正是這樣做的。簡而言之,邏輯查詢計劃優化就是一個利用基於關係代數的等價變換,將高成本的操作替換爲低成本操作的過程。
得到的優化執行計劃在轉換成物 理執行計劃的過程中,還可以根據具體的數據源的特性將過濾條件下推至數據源內。最右側的物理執行計劃中Filter之所以消失不見,就是因爲溶入了用於執行最終的讀取操作的表掃描節點內。
對於普通開發者而言,查詢優化器的意義在於,即便是經驗並不豐富的程序員寫出的次優的查詢,也可以被儘量轉換爲高效的形式予以執行。
Dataframe的劣勢在於在編譯期缺少類型安全檢查,導致運行時出錯.
Dataset
1.是Dataframe API的一個擴展,是Spark最新的數據抽象
2.用戶友好的API風格,既具有類型安全檢查也具有Dataframe的查詢優化特性。
3.Dataset支持編解碼器,當需要訪問非堆上的數據時可以避免反序列化整個對象,提高了效率。
4.樣例類被用來在Dataset中定義數據的結構信息,樣例類中每個屬性的名稱直接映射到DataSet中的字段名稱。
5.Dataframe是Dataset的特列,DataFrame=Dataset[Row] ,所以可以通過as方法將Dataframe轉換爲6.Dataset。Row是一個類型,跟Car、Person這些的類型一樣,所有的表結構信息我都用Row來表示。
7.DataSet是強類型的。比如可以有Dataset[Car],Dataset[Person].
DataFrame只是知道字段,但是不知道字段的類型,所以在執行這些操作的時候是沒辦法在編譯的時候檢查是否類型失敗的,比如你可以對一個String進行減法操作,在執行的時候才報錯,而DataSet不僅僅知道字段,而且知道字段類型,所以有更嚴格的錯誤檢查。就跟JSON對象和類對象之間的類比。
RDD讓我們能夠決定怎麼做,而DataFrame和DataSet讓我們決定做什麼,控制的粒度不一樣。
三者的共性
1、RDD、DataFrame、Dataset全都是spark平臺下的分佈式彈性數據集,爲處理超大型數據提供便利
2、三者都有惰性機制,在進行創建、轉換,如map方法時,不會立即執行,只有在遇到Action如foreach時,三者纔會開始遍歷運算,極端情況下,如果代碼裏面有創建、轉換,但是後面沒有在Action中使用對應的結果,在執行時會被直接跳過.
val sparkconf = new SparkConf().setMaster("local").setAppName("test").set("spark.port.maxRetries","1000")
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 _=>
""
}
三者的區別
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.qianfeng.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.qianfeng.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]就能比較好的解決問題。
其他:
RDD
優點:
- 編譯時類型安全
編譯時就能檢查出類型錯誤 - 面向對象的編程風格
直接通過類名點的方式來操作數據
缺點:
- 序列化和反序列化的性能開銷
無論是集羣間的通信, 還是IO操作都需要對對象的結構和數據進行序列化和反序列化. - GC的性能開銷
頻繁的創建和銷燬對象, 勢必會增加GC
import org.apache.spark.sql.SQLContext
import org.apache.spark.{SparkConf, SparkContext}
object Run {
def main(args: Array[String]) {
val conf = new SparkConf().setAppName("test").setMaster("local")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val sqlContext = new SQLContext(sc)
/**
* id age
* 1 30
* 2 29
* 3 21
*/
case class Person(id: Int, age: Int)
val idAgeRDDPerson = sc.parallelize(Array(Person(1, 30), Person(2, 29), Person(3, 21)))
// 優點1
// idAge.filter(_.age > "") // 編譯時報錯, int不能跟String比
// 優點2
idAgeRDDPerson.filter(_.age > 25) // 直接操作一個個的person對象
}
}
DataFrame
DataFrame引入了schema和off-heap
-
schema : RDD每一行的數據, 結構都是一樣的. 這個結構就存儲在schema中. Spark通過schame就能夠讀懂數據, 因此在通信和IO時就只需要序列化和反序列化數據, 而結構的部分就可以省略了.
-
off-heap : 意味着JVM堆以外的內存, 這些內存直接受操作系統管理(而不是JVM)。Spark能夠以二進制的形式序列化數據(不包括結構)到off-heap中, 當要操作數據時, 就直接操作off-heap內存. 由於Spark理解schema, 所以知道該如何操作.
off-heap就像地盤, schema就像地圖, Spark有地圖又有自己地盤了, 就可以自己說了算了, 不再受JVM的限制, 也就不再收GC的困擾了.
通過schema和off-heap, DataFrame解決了RDD的缺點, 但是卻丟了RDD的優點. DataFrame不是類型安全的, API也不是面向對象風格的.
import org.apache.spark.sql.types.{DataTypes, StructField, StructType}
import org.apache.spark.sql.{Row, SQLContext}
import org.apache.spark.{SparkConf, SparkContext}
object Run {
def main(args: Array[String]) {
val conf = new SparkConf().setAppName("test").setMaster("local")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val sqlContext = new SQLContext(sc)
/**
* id age
* 1 30
* 2 29
* 3 21
*/
val idAgeRDDRow = sc.parallelize(Array(Row(1, 30), Row(2, 29), Row(4, 21)))
val schema = StructType(Array(StructField("id", DataTypes.IntegerType), StructField("age", DataTypes.IntegerType)))
val idAgeDF = sqlContext.createDataFrame(idAgeRDDRow, schema)
// API不是面向對象的
idAgeDF.filter(idAgeDF.col("age") > 25)
// 不會報錯, DataFrame不是編譯時類型安全的
idAgeDF.filter(idAgeDF.col("age") > "")
}
}
DataSet
DataSet結合了RDD和DataFrame的優點, 並帶來的一個新的概念Encoder
當序列化數據時, Encoder產生字節碼與off-heap進行交互, 能夠達到按需訪問數據的效果, 而不用反序列化整個對象. Spark還沒有提供自定義Encoder的API, 但是未來會加入.
下面看DataFrame和DataSet在2.0.0-preview中的實現
下面這段代碼, 在1.6.x中創建的是DataFrame
// 上文DataFrame示例中提取出來的
val idAgeRDDRow = sc.parallelize(Array(Row(1, 30), Row(2, 29), Row(4, 21)))
val schema = StructType(Array(StructField("id", DataTypes.IntegerType), StructField("age", DataTypes.IntegerType)))
val idAgeDF = sqlContext.createDataFrame(idAgeRDDRow, schema)
但是同樣的代碼在2.0.0-preview中, 創建的雖然還叫DataFrame
// sqlContext.createDataFrame(idAgeRDDRow, schema) 方法的實現, 返回值依然是DataFrame
def createDataFrame(rowRDD: RDD[Row], schema: StructType): DataFrame = {
sparkSession.createDataFrame(rowRDD, schema)
}
但是其實卻是DataSet, 因爲DataFrame被聲明爲Dataset[Row]
package object sql {
// ...省略了不相關的代碼
type DataFrame = Dataset[Row]
}
因此當我們從1.6.x遷移到2.0.0的時候, 無需任何修改就直接用上了DataSet.
下面是一段DataSet的示例代碼
import org.apache.spark.sql.types.{DataTypes, StructField, StructType}
import org.apache.spark.sql.{Row, SQLContext}
import org.apache.spark.{SparkConf, SparkContext}
object Test {
def main(args: Array[String]) {
val conf = new SparkConf().setAppName("test").setMaster("local") // 調試的時候一定不要用local[*]
val sc = new SparkContext(conf)
val sqlContext = new SQLContext(sc)
import sqlContext.implicits._
val idAgeRDDRow = sc.parallelize(Array(Row(1, 30), Row(2, 29), Row(4, 21)))
val schema = StructType(Array(StructField("id", DataTypes.IntegerType), StructField("age", DataTypes.IntegerType)))
// 在2.0.0-preview中這行代碼創建出的DataFrame, 其實是DataSet[Row]
val idAgeDS = sqlContext.createDataFrame(idAgeRDDRow, schema)
// 在2.0.0-preview中, 還不支持自定的Encoder, Row類型不行, 自定義的bean也不行
// 官方文檔也有寫通過bean創建Dataset的例子,但是我運行時並不能成功
// 所以目前需要用創建DataFrame的方法, 來創建DataSet[Row]
// sqlContext.createDataset(idAgeRDDRow)
// 目前支持String, Integer, Long等類型直接創建Dataset
Seq(1, 2, 3).toDS().show()
sqlContext.createDataset(sc.parallelize(Array(1, 2, 3))).show()
}
}