基于JVM的開源資料處理語言主要有Kotlin、Scala、SPL,下面對三者進行多方面的橫向比較,從中找出開發效率最高的資料處理語言。本文的适用場景設定為項目開發中常見的資料處理和業務邏輯,以結構化資料為主,大資料和高性能不作為重點,也不涉及消息流、科學計算等特殊場景。
文章目錄
- 基本特征
- 文法
- 資料源
- 結構化資料計算
- 應用結構
- SPL資料
基本特征
适應面
Kotlin的設計初衷是開發效率更高的Java,可以适用于任何Java涉及的應用場景,除了常見的資訊管理系統,還能用于WebServer、Android項目、遊戲開發,通用性比較好。Scala的設計初衷是整合現代程式設計範式的通用開發語言,實踐中主要用于後端大資料處理,其他類型的項目中很少出現,通用性不如Kotlin。SPL的設計初衷是專業的資料處理語言,實踐與初衷一緻,前後端的資料處理、大小資料處理都很适合,應用場景相對聚焦,通用性不如Kotlin。
程式設計範式
Kotlin以面向對象程式設計為主,也支援函數式程式設計。Scala兩種範式都支援,面向對象程式設計比Koltin更徹底,函數式程式設計也比Koltin友善些。SPL可以說不算支援面向對象程式設計,有對象概念,但沒有繼承重載這些内容,函數式程式設計比Kotlin更友善。
運作模式
Kotlin和Scala是編譯型語言,SPL是解釋型語言。解釋型語言更靈活,但相同代碼性能會差一點。不過SPL有豐富且高效的庫函數,總體性能并不弱,面對大資料時常常會更有優勢。
外部類庫
Kotlin可以使用所有的Java類庫,但缺乏專業的資料處理類庫。Scala也可以使用所有的Java類庫,且内置專業的大資料處理類庫(Spark)。SPL内置專業的資料處理函數,提供了大量時間複雜度更低的基本運算,通常不需要外部Java類庫,特殊情況可在自定義函數中調用。
IDE和調試
三者都有圖形化IDE和完整的調試功能。SPL的IDE專為資料處理而設計,結構化資料對象呈現為表格形式,觀察更加友善,Kotlin和Scala的IDE是通用的,沒有為資料處理做優化,無法友善地觀察結構化資料對象。
學習難度
Kotlin的學習難度稍高于Java,精通Java者可輕易學會。Scala的目标是超越Java,學習難度遠大于Java。SPL的目标就是簡化Java甚至SQL的編碼,刻意簡化了許多概念,學習難度很低。
代碼量
Kotlin的初衷是提高Java的開發效率,官方宣稱綜合代碼量隻有Java的20%,可能是資料處理類庫不專業的緣故,這方面的實際代碼量降低不多。Scala的文法糖不少,大資料處理類庫比較專業,代碼量反而比Kotlin低得多。SPL隻用于資料處理,專業性最強,再加上解釋型語言表達能力強的特點,完成同樣任務的代碼量遠遠低于前兩者(後面會有對比例子),從另一個側面也能說明其學習難度更低。
文法
資料類型
- 原子資料類型:三者都支援,比如Short、Int、Long、Float、Double、Boolean
- 日期時間類型:Kotlin缺乏易用的日期時間類型,一般用Java的。Scala和SPL都有專業且友善的日期時間類型。
- 有特色的資料類型:Kotlin支援非數值的字元Char、可空類型Any?。Scala支援元組(固定長度的泛型集合)、内置BigDecimal。SPL支援高性能多層序号鍵,内置BigDecimal。
- 集合類型:Kotlin和Scala支援Set、List、Map。SPL支援序列(有序泛型集合,類似List)。
- 結構化資料類型:Kotlin有記錄集合List<EntityBean>,但缺乏中繼資料,不夠專業。Scala有專業的結構化數類型,包括Row、RDD、DataSet、DataFrame(本文以此為例進行說明)等。SPL有專業的結構化資料類型,包括record、序表(本文以此為例進行說明)、内表壓縮表、外存Lazy遊标等。
Scala獨有隐式轉換能力,理論上可以在任意資料類型之間進行轉換(包括參數、變量、函數、類),可以友善地改變或增強原有功能。
流程處理
三者都支援基礎的順序執行、判斷分支、循環,理論上可進行任意複雜的流程處理,這方面不多讨論,下面重點比較針對集合資料的循環結構是否友善。以計算比上期為例,Kotlin代碼:
mData.forEachIndexed{index,it->
if(index>0) it.Mom= it.Amount/mData[index-1].Amount-1
}
Kotlin的forEachIndexed函數自帶序号變量和成員變量,進行集合循環時比較友善,支援下标取記錄,可以友善地進行跨行計算。Kotlin的缺點在于要額外處理數組越界。
Scala代碼:
val w = Window.orderBy(mData("SellerId"))
mData.withColumn("Mom", mData ("Amount")/lag(mData ("Amount"),1).over(w)-1)
Scala跨行計算不必處理數組越界,這一點比Kotlin友善。但Scala的結構化資料對象不支援下标取記錄,隻能用lag函數整體移行,這對結構化資料不夠友善。lag函數不能用于通用性強的forEach,而要用withColumn之類功能單一的循環函數。為了保持函數式程式設計風格和SQL風格的底層統一,lag函數還必須配合視窗函數(Python的移行函數就沒這種要求),整體代碼看上去反而比Kotlin複雜。
SPL代碼:
mData.(Mom=Amount/Amount[-1]-1)
SPL對結構化資料對象的流程控制進行了多項優化,類似forEach這種最通用最常用的循環函數,SPL可以直接用括号表達,簡化到極緻。SPL也有移行函數,但這裡用的是更符合直覺的“[相對位置]"文法,進行跨行計算時比Kotlin的絕對定位強大,比Scala的移行函數友善。上述代碼之外,SPL還有更多針對結構化資料的流程處理功能,比如:每輪循環取一批而不是一條記錄;某字段值變化時循環一輪。
Lambda表達式
Lambda表達式是匿名函數的簡單實作,目的是簡化函數的定義,尤其是變化多樣的集合計算類函數。Kotlin支援Lambda表達式,但因為編譯型語言的關系,難以将參數表達式友善地指定為值參數或函數參數,隻能設計複雜的接口規則進行區分,甚至有所謂高階函數專用接口,這就導緻Kotin的Lambda表達式編寫困難,在資料處理方面專業性不足。幾個例子:
"abcd".substring( 1,2) //值參數
"abcd".sumBy{ it.toInt()} //函數參數
mData.forEachIndexed{ index,it-> if(index>0) it.Mom=…} //函數參數的函數帶多個參數
Koltin的Lambda表達式專業性不足,還表現在使用字段時必須帶上結構化資料對象的變量名(it),而不能像SQL那樣單表計算時可以省略表名。
同為編譯型語言,Scala的Lambda表達式和Kotlin差別不大,同樣需要設計複雜的接口規則,同樣編寫困難,這裡就不舉例了。計算比上期時,字段前也要帶上結構化資料對象變量名或用col函數,形如mData (“Amount”)或col(“Amount”),雖然可以用文法糖彌補,寫成$”Amount”或’Amount,但很多函數不支援這種寫法,硬要彌補反而使風格不統一。
SPL的Lambda表達式簡單易用,比前兩者更專業,這與其解釋型語言的特性有關。解釋型語言可以友善地推斷出值參數和函數參數,沒有所謂複雜的高階函數專用接口,所有的函數接口都一樣簡單。幾個例子:
mid("abcd",2,1) //值參數
Orders.sum(Amount*Amount) //函數參數
mData.(Mom=Amount/Amount[-1]-1) //函數參數的函數帶多個參數
SPL可直接使用字段名,無須結構化資料對象變量名,比如:
Orders.select(Amount>1000 && Amount<=3000 && like(Client,"*S*"))
SPL的大多數循環函數都有預設的成員變量~和序号變量#,可以顯著提升代碼編寫的便利性,特别适合結構化資料計算。比如,取出偶數位置的記錄:
Students.select(# % 2==0)
求各組的前3名:
Orders.group(SellerId;~.top(3;Amount))
SPL函數選項和層次參數
值得一提的是,為了進一步提高開發效率,SPL還提供了獨特的函數文法。
有大量功能類似的函數時,大部分程式語言隻能用不同的名字或者參數進行區分,使用不太友善。而SPL提供了非常獨特的函數選項,使功能相似的函數可以共用一個函數名,隻用函數選項區分差别。比如,select函數的基本功能是過濾,如果隻過濾出符合條件的第1條記錄,可使用選項@1:
T.select@1(Amount>1000)
對有序資料用二分法進行快速過濾,使用@b:
T.select@b(Amount>1000)
函數選項還可以組合搭配,比如:
Orders.select@1b(Amount>1000)
有些函數的參數很複雜,可能會分成多層。正常程式語言對此并沒有特别的文法方案,隻能生成多層結構資料對象再傳入,非常麻煩。SQL使用了關鍵字把參數分隔成多個組,更直覺簡單,但這會動用很多關鍵字,使語句結構不統一。而SPL創造性地發明了層次參數簡化了複雜參數的表達,通過分号、逗号、冒号自高而低将參數分為三層:
join(Orders:o,SellerId ; Employees:e,EId)
資料源
資料源種類
Kotlin原則上可以支援所有的Java資料源,但代碼很繁瑣,類型轉換麻煩,穩定性也差,這是因為Kotlin沒有内置的資料源通路接口,更沒有針對結構化資料處理做優化(JDBC接口除外)。從這個意義講,也可以說它不直接支援任何資料源,隻能使用Java第三方類庫,好在第三方類庫的數量足夠龐大。
Scala支援的資料源種類比較多,且有六種資料源接口是内置的,并針對結構化資料處理做了優化,包括:JDBC、CSV、TXT、JSON、Parquet列存格式、ORC列式存儲,其他的資料源接口雖然沒有内置,但可以用社群小組開發的第三方類庫。Scala提供了資料源接口規範,要求第三方類庫輸出為結構化資料對象,常見的第三方接口有XML、Cassandra、HBase、MongoDB等。
SPL内置了最多的資料源接口,并針對結構化資料處理做了優化,包括:
- JDBC(即所有的RDB)
- CSV、TXT、JSON、XML、Excel
- HBase、HDFS、Hive、Spark
- Salesforce、阿裡雲
- Restful、WebService、Webcrawl
- Elasticsearch、MongoDB、Kafka、R2dbc、FTP
- Cassandra、DynamoDB、influxDB、Redis、SAP
這些資料源都可以直接使用,非常友善。對于其他未列入的資料源,SPL也提供了接口規範,隻要按規範輸出為SPL的結構化資料對象,就可以進行後續計算。
代碼比較
以規範的CSV檔案為例,比較三種語言的解析代碼。Kotlin:
val file = File("D:\\data\\Orders.txt")
data class Order(var OrderID: Int,var Client: String,var SellerId: Int, var Amount: Double, var OrderDate: Date)
var sdf = SimpleDateFormat("yyyy-MM-dd")
var Orders=file.readLines().drop(1).map{
var l=it.split("\t")
var r=Order(l[0].toInt(),l[1],l[2].toInt(),l[3].toDouble(),sdf.parse(l[4]))
r
}
var resutl=Orders.filter{
it.Amount>= 1000 && it.Amount < 3000}
Koltin專業性不足,通常要硬寫代碼讀取CSV,包括事先定義資料結構,在循環函數中手工解析資料類型,整體代碼相當繁瑣。也可以用OpenCSV等類庫讀取,資料類型雖然不用在代碼中解析,但要在配置檔案中定義,實作過程不見得簡單。
Scala專業性強,内置解析CSV的接口,代碼比Koltin簡短得多:
val spark = SparkSession.builder().master("local").getOrCreate()
val Orders = spark.read.option("header", "true").option("sep","\t").option("inferSchema", "true").csv("D:/data/orders.csv").withColumn("OrderDate", col("OrderDate").cast(DateType))
Orders.filter("Amount>1000 and Amount<=3000")
Scala在解析資料類型時麻煩些,其他方面沒有明顯缺點。
SPL更加專業,連解析帶計算隻要一行:
T("D:/data/orders.csv").select(Amount>1000 && Amount<=3000)
跨源計算
JVM資料處理語言的開放性強,有足夠的能力對不同的資料源進行關聯、歸并、集合運算,但資料處理專業性的差異,導緻不同語言的友善程度差別較大。
Kotlin不夠專業,不僅缺乏内置資料源接口,也缺乏跨源計算函數,隻能硬寫代碼實作。假設已經從不同資料源獲得了員工表和訂單表,現在把兩者關聯起來:
data class OrderNew(var OrderID:Int ,var Client:String, var SellerId:Employee ,var Amount:Double ,var OrderDate:Date )
val result = Orders.map { o->var emp=Employees.firstOrNull{ it.EId==o.SellerId
}
emp?.let{ OrderNew(o.OrderID,o.Client,emp,o.Amount,o.OrderDate)
}
}
.filter {o->o!=null}
很容易看出Kotlin的缺點,代碼隻要一長,Lambda表達式就變得難以閱讀,還不如普通代碼好了解;關聯後的資料結構需要事先定義,靈活性差,影響解題流暢性。
Scala比Kotlin專業,不僅内置了多種資料源接口,而且提供了跨源計算的函數。同樣的計算,Scala代碼簡單多了:
val join=Orders.join(Employees,Orders("SellerId")===Employees("EId"),"Inner")
可以看到,Scala不僅具備專用于結構化資料計算的對象和函數,而且可以很好地配合Lambda語言,代碼更易了解,也不用事先定義資料結構。
SPL更加專業,結構化資料對象更專業,跨源計算函數更友善,代碼更簡短:
join(Orders:o,SellerId;Employees:e,EId)
自有存儲格式
反複使用的中間資料,通常會以某種格式存為本地檔案,以此提高取數性能。Kotlin支援多種格式的檔案,理論上能夠進行中間資料的存儲和再計算,但因為在資料處理方面不專業,基本的讀寫操作都要寫大段代碼,相當于并沒有自有的存儲格式。
Scala支援多種存儲格式,其中parquet檔案常用且易用。parquet是開源存儲格式,支援列存,可存儲大量資料,中間計算結果(DataFrame)可以和parquet檔案友善地互轉。遺憾的是,parquet的索引尚不成熟。
val df = spark.read.parquet("input.parquet")
val result=df.groupBy(data("Dept"),data("Gender")).agg(sum("Amount"),count("*"))
result.write.parquet("output.parquet")
SPL支援btx和ctx兩種私有二進制存儲格式,btx是簡單行存,ctx支援行存、列存、索引,可存儲大量資料并進行高性能計算,中間計算結果(序表/遊标)可以和這兩種檔案友善地互轉。
A | |
1 | =file("input.ctx").open() |
2 | =A1.cursor(Dept,Gender,Amount).groups(Dept,Gender;sum(Amount):amt,count(1):cnt) |
3 | =file("output.ctx").create(#Dept,#Gender,amt,cnt).append(A2.cursor()) |
結構化資料計算
結構化資料對象
資料處理的核心是計算,尤其是結構化資料的計算。結構化資料對象的專業程度,深刻地決定了資料處理的友善程度。
Kotlin沒有專業的結構化資料對象,常用于結構化資料計算的是List<EntityBean>,其中EntityBean可以用data class簡化定義過程。
List是有序集合(可重複),凡涉及成員序号和集合的功能,Kotlin支援得都不錯。比如按序号通路成員:
Orders[3] //按下标取記錄,從0開始
Orders.take(3) //前3條記錄
Orders.slice(listOf(1,3,5)+IntRange(7,10)) //下标是1、3、5、7-10的記錄
還可以按倒數序号取成員:
Orders.reversed().slice(1,3,5) //倒數第1、3、5條
Orders.take(1)+Orders.takeLast(1) //第1條和最後1條
涉及順序的計算難度都比較大,Kotlin支援有序計集合,進行相關的計算會比較友善。作為集合的一種,List擅長的功能還有集合成員的增删改、交差合、拆分等。但List不是專業的結構化資料對象,一旦涉及字段結構相關的功能,Kotlin就很難實作了。比如,取Orders中的兩個字段組成新的結構化資料對象。
data class CliAmt(var Client: String, var Amount: Double)
var CliAmts=Orders.map{it.let{CliAmt(it.Client,it.Amount) }}
上面的功能很常用,相當于簡單SQL語句select Client,Amount from Orders,但Kotlin寫起來就很繁瑣,不僅要事先定義新結構,還要寫死完成字段的指派。簡單的取字段功能都這麼繁瑣,進階些的功能就更麻煩了,比如:按字段序号取、按參數取、獲得字段名清單、修改字段結構、在字段上定義鍵和索引、按字段查詢計算。
Scala也有List,與Kotlin差別不大,但Scala為結構化資料處理設計了更加專業的資料對象DataFrame(以及RDD、DataSet)。
DataFrame是有結構的資料流,與資料庫結果集有些相似,都是無序集合,是以不支援按下标取數,隻能變相實作。比如,第10條記錄:
Orders.limit(10).tail(1)(0)
可以想象,凡與順序相關的計算,DataFrame實作起來都比較麻煩,比如區間、移動平均、倒排序等。
除了資料無序,DataFrame也不支援修改(immutable特性),如果想改變資料或結構,必須生成新的DataFrame。比如修改字段名,實際上要通過複制記錄來實作:
Orders.selectExpr("Client as Cli")
DataFrame支援常見的集合計算,比如拆分、合并、交差合并,其中并集可通過合集去重實作,但因為要通過複制記錄來實作,集合計算的性能普遍不高。
雖然有不少缺點,但DataFrame是專業的結構化資料對象,字段通路方面的能力是Kotlin無法企及的。比如,獲得中繼資料/字段名清單:
Orders.schema.fields.map(it=>it.name).toList
還可以友善地用字段取數,比如,取兩個字段形成新dataframe:
Orders.select("Client","Amount") //可以隻用字段名
或用計算列形成新DataFrame:
Orders.select(Orders("Client"),Orders("Amount")+1000) //不能隻用字段名
遺憾的是,DataFrame隻支援用字元串形式的名字來引用字段,不支援用字段序号或預設名字,導緻很多場景下不夠友善。此外,DataFrame也不支援定義索引,無法進行高性能随機查詢,專業性還有缺陷。
SPL的結構化資料對象是序表,優點是足夠專業,簡單易用,表達能力強。
按序号通路成員:
Orders(3) //按下标取記錄,從1開始
Orders.to(3) //前3條記錄
Orders.m(1,3,5,7:10) //序号是1、3、5、7-10的記錄
按倒數序号取記錄,獨特之處在于支援負号表示倒數,比Kotlin專業且友善:
Orders.m(-1,-3,-5) //倒數第1,3,5條
Orders.m(1,-1) //第1條和最後1條
作為集合的一種,序表也支援集合成員的增删改、交并差合、拆分等功能。由于序表和List一樣都是可變集合(mutable),集合計算時盡可能使用遊離記錄,而不是複制記錄,性能比Scala好得多,記憶體占用也少。
序表是專業的結構化資料對象,除了集合相關功能外,更重要的是可以友善地通路字段。比如,獲得字段名清單:
Orders.fname()
取兩個字段形成新序表:
Orders.new(Client,Amount)
用計算列形成新序表:
Orders.new(Client,Amount*0.2)
修改字段名:
Orders.alter(;OrderDate) //不複制記錄
有些場景需要用字段序号或預設名字通路字段,SPL都提供了相應的通路方法:
Orders(Client) //按字段名(表達式取)
Orders([#2,#3]) //按預設字段名取
Orders.field(“Client”) //按字元串(外部參數)
Orders.field(2) //按字段序号取
作為專業的結構化資料對象,序表還支援在字段上定義鍵和索引:
Orders.keys@i(OrderID) //定義鍵,同時建立哈希索引
Orders.find(47) //用索引高速查找
計算函數
Kotlin支援部分基本計算函數,包括:過濾、排序、去重、集合的交叉合并、各類聚合、分組彙總。但這些函數都是針對普通集合的,如果計算目标改成結構化資料對象,計算函數庫就顯得非常不足,通常就要輔以寫死才能實作計算。還有很多基本的集合運算是Kotlin不支援的,隻能自行編碼實作,包括:關聯、視窗函數、排名、行轉列、歸并、二分查找等。其中,歸并和二分查找等屬于次序相關的運算,由于Kotlin List是有序集合,自行編碼實作這類運算不算太難。總體來講,面對結構化資料計算,Kotlin的函數庫可以說較弱。
Scala的計算函數比較豐富,且都是針對結構化資料對象設計的,包括Kotlin不支援的函數:排名、關聯、視窗函數、行轉列,但基本上還沒有超出SQL的架構。也有一些基本的集合運算是Scala不支援的,尤其是與次序相關的,比如歸并、二分查找,由于Scala DataFrame沿用了SQL中資料無序的概念,即使自行編碼實作此類運算,難度也是非常大的。總的來說,Scala的函數庫比Kotlin豐富,但基本運算仍有缺失。
SPL的計算函數最豐富,且都是針對結構化資料對象設計的,SPL極大地豐富了結構化資料運算内容,設計了很多超出SQL的内容,當然也是Scala/Kotlin不支援的函數,比如有序計算:歸并、二分查找、按區間取記錄、符合條件的記錄序号;除了正常等值分組,還支援枚舉分組、對齊分組、有序分組;将關聯類型分成外鍵和主子;支援主鍵以限制資料,支援索引以快速查詢;對多層結構的資料(多表關聯或Json\XML)進行遞歸查詢等。
以分組為例,除了正常的等值分組外,SPL還提供了更多的分組方案:
- 枚舉分組:分組依據是若幹條件表達式,符合相同條件的記錄分為一組。
- 對齊分組:分組依據是外部集合,記錄的字段值與該集合的成員相等的分為一組,組的順序與該集合成員的順序保持一緻,允許有空組,可單獨分出一組“不屬于該集合的記錄”。
- 有序分組:分組依據是已經有序的字段,比如字段發生變化或者某個條件成立時分出一個新組,SPL直接提供了這類有序分組,在正常分組函數上加個選項就可以完成,非常簡單而且運算性能也更好。其他語言(包括SQL)都沒有這種分組,隻能費勁地轉換為傳統的等值分組或者自己寫死實作。
下面我們通過幾個正常例子來感受一下這三種語言在計算函數方式的差異。
排序
按Client順序,Amount逆序排序。Kotlin:
Orders.sortedBy{it.Amount}.sortedByDescending{it.Client}
Kotlin代碼不長,但仍有不便之處,包括:逆序正序是兩個不同的函數,字段名必須帶表名,代碼寫出的字段順序與實際的排序順序相反。
Scala:
Orders.orderBy(Orders("Client"),-Orders("Amount"))
Scala簡單多了,負号代表逆序,代碼寫出的字段順序與排序的順序相同。遺憾之處在于:字段仍要帶表名;編譯型語言隻能用字元串實作表達式的動态解析,導緻代碼風格不統一。
SPL:
Orders.sort(Client,-Amount)
SPL代碼更簡單,字段不必帶表名,解釋型語言代碼風格容易統一。
分組彙總
Kotlin:
data class Grp(var Dept:String,var Gender:String)
data class Agg(var sumAmount: Double,var rowCount:Int)
var result1=data.groupingBy{Grp(it!!.Dept,it.Gender)}
.fold(Agg(0.0,0),{acc, elem -> Agg(acc.sumAmount + elem!!.Amount,acc.rowCount+1)})
.toSortedMap(compareBy<Grp> { it.Dept }.thenBy { it.Gender })
Kotlin代碼比較繁瑣,不僅要用groupingBy和fold函數,還要輔以寫死才能實作分組彙總。當出現新的資料結構時,必須事先定義才能用,比如分組的雙字段結構、彙總的雙字段結構,這樣不僅靈活性差,而且影響解題流暢性。最後的排序是為了和其他語言的結果順序保持一緻,不是必須的。
Scala:
val result=data.groupBy(data("Dept"),data("Gender")).agg(sum("Amount"),count("*"))
Scala代碼簡單多了,不僅易于了解,而且不用事先定義資料結構。
SPL:
data.groups(Dept,Gender;sum(Amount),count(1))
SPL代碼最簡單,表達能力不低于SQL。
關聯計算
兩個表有同名字段,對其關聯并分組彙總。Kotlin代碼:
data class OrderNew(var OrderID:Int ,var Client:String, var SellerId:Employee ,var Amount:Double ,var OrderDate:Date )
val result = Orders.map { o->var emp=Employees.firstOrNull{it.EId==o.EId}
emp?.let{ OrderNew(o.OrderID,o.Client,emp,o.Amount,o.OrderDate)}
}
.filter {o->o!=null}
data class Grp(var Dept:String,var Gender:String)
data class Agg(var sumAmount: Double,var rowCount:Int)
var result1=data.groupingBy{Grp(it!!.EId.Dept,it.EId.Gender)}
.fold(Agg(0.0,0),{acc, elem -> Agg(acc.sumAmount + elem!!.Amount,acc.rowCount+1)})
.toSortedMap(compareBy<Grp> { it.Dept }.thenBy { it.Gender })
Kotlin代碼很繁瑣,很多地方都要定義新資料結構,包括關聯結果、分組的雙字段結構、彙總的雙字段結構。
Scala
val join=Orders.as("o").join(Employees.as("e"),Orders("EId")===Employees("EId"),"Inner")
val result= join.groupBy(join("e.Dept"), join("e.Gender")).agg(sum("o.Amount"),count("*"))
Scala比Kolin簡單多了,不用繁瑣地定義資料結構,也不必寫死。
SPL更簡單:
join(Orders:o,SellerId;Employees:e,EId).groups(e.Dept,e.Gender;sum(o.Amount),count(1))
綜合資料處理對比
CSV内容不規範,每三行對應一條記錄,其中第二行含三個字段(即集合的集合),将該檔案整理成規範的結構化資料對象,并按第3和第4個字段排序.
Kotlin:
data class Order(var OrderID: Int,var Client: String,var SellerId: Int, var Amount: Double, var OrderDate: Date)
var Orders=ArrayList<Order>()
var sdf = SimpleDateFormat("yyyy-MM-dd")
var raw=File("d:\\threelines.txt").readLines()
raw.forEachIndexed{index,it->
if(index % 3==0) {
var f234=raw[index+1].split("\t")
var r=Order(raw[index].toInt(),f234[0],f234[1].toInt(),f234[2].toDouble(),
sdf.parse(raw[index+2]))
Orders.add(r)
}
}
var result=Orders.sortedByDescending{it.Amount}.sortedBy{it.SellerId}
Koltin在資料處理方面專業性不足,大部分功能要硬寫代碼,包括按位置取字段、從集合的集合取字段。
Scala:
val raw=spark.read.text("D:/threelines.txt")
val rawrn=raw.withColumn("rn", monotonically_increasing_id())
var f1=rawrn.filter("rn % 3==0").withColumnRenamed("value","OrderId")
var f5=rawrn.filter("rn % 3==2").withColumnRenamed("value","OrderDate")
var f234=rawrn.filter("rn % 3==1")
.withColumn("splited",split(col("value"),"\t"))
.select(col("splited").getItem(0).as("Client")
,col("splited").getItem(1).as("SellerId")
,col("splited").getItem(2).as("Amount"))
f1.withColumn("rn1",monotonically_increasing_id())
f5=f5.withColumn("rn1",monotonically_increasing_id())
f234=f234.withColumn("rn1",monotonically_increasing_id())
var f=f1.join(f234,f1("rn1")===f234("rn1"))
.join(f5,f1("rn1")===f5("rn1"))
.select("OrderId","Client","SellerId","Amount","OrderDate")
val result=f.orderBy(col("SellerId"),-col("Amount"))
Scala在資料處理方面更加專業,大量使用結構化計算函數,而不是硬寫循環代碼。但Scala缺乏有序計算能力,相關的功能通常要添加序号列再處理,導緻整體代碼冗長。
SPL:
A | |
1 | =file("D:\\data.csv").import@si() |
2 | =A1.group((#-1)\3) |
3 | =A2.new(~(1):OrderID, (line=~(2).array("\t"))(1):Client,line(2):SellerId,line(3):Amount,~(3):OrderDate ) |
4 | =A3.sort(SellerId,-Amount) |
SPL在資料處理方面最專業,隻用結構化計算函數就可以實作目标。SPL支援有序計算,可以直接按位置分組,按位置取字段,從集合中的集合取字段,雖然實作思路和Scala類似,但代碼簡短得多。
應用結構
Java應用內建
Kotlin編譯後是位元組碼,和普通的class檔案一樣,可以友善地被Java調用。比如KotlinFile.kt裡的靜态方法fun multiLines(): List<Order>,會被Java正确識别,直接調用即可:
java.util.List result=KotlinFileKt.multiLines();
result.forEach(e->{System.out.println(e);});
Scala編譯後也是位元組碼,同樣可以友善地被Java調用。比如ScalaObject對象的靜态方法def multiLines():DataFrame,會被Java識别為Dataset類型,稍做修改即可調用:
org.apache.spark.sql.Dataset df=ScalaObject.multiLines();
df.show();
SPL提供了通用的JDBC接口,簡單的SPL代碼可以像SQL一樣,直接嵌入Java:
Class.forName("com.esproc.jdbc.InternalDriver");
Connection connection =DriverManager.getConnection("jdbc:esproc:local://");
Statement statement = connection.createStatement();
String str="=T(\"D:/Orders.xls\").select(Amount>1000 && Amount<=3000 && like(Client,\"*s*\"))";
ResultSet result = statement.executeQuery(str);
複雜的SPL代碼可以先存為腳本檔案,再以存儲過程的形式被Java調用,可有效降低計算代碼和前端應用的耦合性。
Class.forName("com.esproc.jdbc.InternalDriver");
Connection conn =DriverManager.getConnection("jdbc:esproc:local://");
CallableStatement statement = conn.prepareCall("{call scriptFileName(?, ?)}");
statement.setObject(1, "2020-01-01");
statement.setObject(2, "2020-01-31");
statement.execute();
SPL是解釋型語言,修改後不用編譯即可直接執行,支援代碼熱切換,可降低維護工作量,提高系統穩定性。Kotlin和Scala是編譯型語言,編譯後必須擇時重新開機應用。
互動式指令行
Kotlin的互動式指令行需要額外下載下傳,使用Kotlinc指令啟動。Kotlin指令行理論上可以進行任意複雜的資料處理,但因為代碼普遍較長,難以在指令行修改,還是更适合簡單的數字計算:
>>>Math.sqrt(5.0)
2.236.6797749979
Scala的互動式指令行是内置的,使用同名指令啟動。Scala指令行理論上可以進行資料處理,但因為代碼比較長,更适合簡單的數字計算:
scala>100*3
rest1: Int=300
(1): T("d:/Orders.txt").groups(SellerId;sum(Amount):amt).select(amt>2000)
(2):^C
D:\raqsoft64\esProc\bin>Log level:INFO
1 4263.900000000001
3 7624.599999999999
4 14128.599999999999
5 26942.4