JAVA開發中經常會遇到不方便使用資料庫,但又要進行結構化資料計算的場景。JAVA早期沒有提供相關類庫,即使排序、分組這種基本計算也要硬寫程式碼,開發效率很低。後來JAVA8推出了Stream庫,憑藉Lambda表示式、鏈式程式設計風格、集合函數,才終於解決了結構化資料計算類庫從無到有的問題。
比如排序:
Stream<Order> result=Orders
.sorted((sAmount1,sAmount2)->Double.compare(sAmount1.Amount,sAmount2.Amount))
.sorted((sClient1,sClient2)->CharSequence.compare(sClient2.Client,sClient1.Client));
上面程式碼中的sorted是集合函數,可方便地進行排序。"(引數)->函數體"的寫法即Lambda表示式,可以簡化匿名函數的定義。兩個sorted函數連在一起用屬於鏈式程式設計風格,可以使多步驟計算變得直觀。
仍然以上面的排序為例,sorted函數只需要知道排序欄位和順序/逆序就夠了,參考SQL的寫法"…from Orders order by Client desc, Amount",但實際上還要額外輸入排序欄位的資料型別。順序/逆序用asc/desc(或+/-)等符號就可以簡單表示了,但這裡卻要用compare函數。另外,實際要排序的欄位順序和程式碼寫出來的順序是相反的,有些反直覺。再比如分組彙總:
Calendar cal=Calendar.getInstance();
Map<Object, DoubleSummaryStatistics> c=Orders.collect(Collectors.groupingBy(
r->{
cal.setTime(r.OrderDate);
return cal.get(Calendar.YEAR)+"_"+r.SellerId;
},
Collectors.summarizingDouble(r->{
return r.Amount;
})
)
);
for(Object sellerid:c.keySet()){
DoubleSummaryStatistics r =c.get(sellerid);
String year_sellerid[]=((String)sellerid).split("_");
System.out.println("group is (year):"+year_sellerid[0]+"\t (sellerid):"+year_sellerid[1]+"\t sum is:"+r.getSum()+"\t count is:"+r.getCount());
}
上面程式碼中,所有出現欄位名的地方,都要先寫上表名,即"表名.欄位名",而不能像SQL那樣省略表名。匿名函數語法複雜,隨著程式碼量的增加,複雜度迅速增長。兩個匿名函數形成巢狀,程式碼更難解讀。實現一個分組彙總功能要用多個函數和類,包括groupingBy、collect、Collectors、summarizingDouble、DoubleSummaryStatistics等,學習成本不低。分組彙總的結果是Map,而不是結構化資料型別,如果要繼續計算,通常要定義新的結構化資料型別,並進行轉換型別,處理過程很繁瑣。兩個分組欄位在結構化資料計算中很常見,但函數grouping只支援一個分組變數,為了讓一個變數代表兩個欄位,就要採取一些變通技巧,比如新建一個兩欄位的結構化資料型別,或者把兩個欄位用下劃線拼起來,這讓程式碼變得更加繁瑣。
「Stream計算能力不足,原因在於其基礎語言JAVA是編譯型語言,無法提供專業的結構化資料物件,缺少來自底層的有力支援。」
JAVA是編譯型語言,返回值的結構必須事先定義,遇到較多的中間步驟時,就要定義多個資料結構,這不僅讓程式碼變得繁瑣,還導致引數處理不靈活,要用一套複雜的規則來實現匿名語法。解釋性語言則天然支援動態結構,還可以方便地將參數列達式指定為值引數或函數引數,提供更簡單的匿名函數。
在這種情況下,Kotlin應運而生。Kotlin是基於JAVA的現代開發語言,所謂現代,重點體現在對JAVA語法尤其是Stream的改進上,即Lambda表示式更加簡潔,集合函數更加豐富。
比如排序:
var resutl=Orders.sortedBy{it.Amount}.sortedByDescending{it.Client}
上面程式碼無須指明排序欄位的資料型別,無須用函數表達順序/逆序,直接參照it作為匿名函數的預設引數,而不是刻意定義,整體比Stream簡短不少。
仍然以排序為例,Kotlin雖然提供了it這個預設引數,但理論上只要知道欄位名就夠了,沒必要帶上表名(it)。排序函數只能對一個欄位進行排序,不能動態接收多個欄位。
再比如分組彙總:
data class Grp(var OrderYear:Int,var SellerId:Int)
data class Agg(var sumAmount: Double,var rowCount:Int)
var result=Orders.groupingBy{Grp(it.OrderDate.year+1900,it.SellerId)}
.fold(Agg(0.0,0),{
acc, elem -> Agg(acc.sumAmount + elem.Amount,acc.rowCount+1)
})
.toSortedMap(compareBy<Grp> { it. OrderYear}.thenBy { it. SellerId})
result.forEach{println("group fields:${it.key.OrderYear}\t${it.key.SellerId}\t aggregate fields:${it.value.sumAmount}\t${it.value.rowCount}") }
上面程式碼中,一個分組彙總的動作,需要用到多個函數,包括複雜的巢狀函數。用到欄位的地方要帶上表名。分組彙總的結果不是結構化資料型別。要事先定義中間結果的資料結構。
如果繼續考察集合、關聯等更多的計算,就會發現同樣的規律:Kotlin程式碼的確比Stream短一些,但大都是無關緊要的量變,並未發生深刻的質變,該有的步驟一個不少。
Kotlin也不支援動態資料結構,無法提供專業的結構化資料物件,難以真正簡化Lambda語法,無法脫離表名直接參照欄位,無法直接支援動態的多欄位計算(比如多欄位排序)。
esProc SPL的出現,將會徹底改觀JAVA生態下結構化資料處理的困境。
esProc SPL是JVM下的開源結構化資料計算語言,提供了專業的結構化資料物件,內建豐富的計算函數,靈活簡潔的語法,易於整合的JDBC介面,擅長簡化複雜計算。
比如排序:=Orders.sort(-Client, Amount)
SPL無須指明排序欄位的資料型別,無須用函數指明方向/逆序,使用欄位時無須附帶表名,一個函數就可以動態地對多個欄位進行排序。
分組彙總:=Orders.groups(year(OrderDate),Client; sum(Amount),count(1))
上面的計算結果仍然是結構化資料物件,可以直接參與下一步計算。對雙欄位進行分組或彙總時,也不需要事先定義資料結構。整體程式碼沒有多餘的函數,sum和count用法簡潔易懂,甚至很難覺察這是巢狀的匿名函數。
更多計算也同樣簡單:
去重:=Orders.id(Client)
模糊查詢:=Orders.select(Amount*Quantity>3000 && like(Client,"S"))
關聯:=join(Orders:o,SellerId ; Employees:e,EId).groups(e.Dept; sum(o.Amount))
Class.forName("com.esproc.jdbc.InternalDriver");
Connection connection =DriverManager.getConnection("jdbc:esproc:local://");
Statement statement = connection.createStatement();
String str="=T(\"D:/Orders.xls\"). Orders.groups(year(OrderDate),Client; sum(Amount))";
ResultSet result = statement.executeQuery(str);
SPL可簡化分步計算、有序計算、分組後計算等邏輯較複雜的計算,很多SQL/儲存過程難以實現的計算,用SPL解決起來就很輕鬆。比如,找出銷售額累計佔到一半的前n個大客戶,並按銷售額從大到小排序:
A | B | |
1 | … | /取資料 |
2 | =A1.sort(amount:-1) | /銷售額逆序排序 |
3 | =A2.cumulate(amount) | /計算累計序列 |
4 | =A3.m(-1)/2 | /最後的累計即總額 |
5 | =A3.pselect(~>=A4) | /超過一半的位置 |
6 | =A2(to(A5)) | /按位元置取值 |
除了計算能力,SPL在系統架構、資料來源、中間資料儲存、計算效能上也有一些特有的優勢,這些優勢有助於SPL進行庫外結構化資料計算。
比如,將上面的SPL程式碼存為指令碼檔案,再在JAVA中以儲存過程的形式呼叫檔名:
Class.forName("com.esproc.jdbc.InternalDriver");
Connection connection =DriverManager.getConnection("jdbc:esproc:local://");
Statement statement = connection.createStatement();
ResultSet result = statement.executeQuery("call getClient()");
SPL是直譯語言,修改後可直接執行,無須編譯,不必重啟JAVA服務。SPL程式碼外接於JAVA,通過檔名被呼叫,不依賴JAVA程式碼,耦合性低。
SPL支援各類資料庫,txt\csv\xls等檔案,MongoDB、Hadoop、redis、ElasticSearch、Kafka、Cassandra等NoSQL,特別地,還支援WebService XML、Restful Json等多層資料:
A | |
1 | =json(file("d:/Orders.json").read()) |
2 | =json(A1).conj() |
3 | =A2.select(Amount>p_start && Amount<=p_end) |
對文字檔案和資料庫進行跨源關聯:
A | |
1 | =T("Employees.csv") |
2 | =mysql1.cursor("select SellerId, Amount from Orders order by SellerId") |
3 | =joinx(A2:O,SellerId; A1:E,EId) |
4 | =A3.groups(E.Dept;sum(O.Amount)) |
SPL支援btx儲存格式,適合暫存來自於低速資料來源的資料,比如CSV:
A | B | |
1 | =[T("d:/orders1.csv"), T("d:/orders2.csv")].merge@u() | /對記錄做並集 |
2 | file("d:/fast.btx").export@b(A1) | /寫入集檔案 |
btx體積小,讀寫速度快,可以像普通文字檔案那樣進行計算:
=T("D:/fast.btx").sort(Client,- Amount)
如果對btx進行有序儲存,還能獲得高計算效能,比如平行計算、二分查詢。SPL還支援更高效能的ctx儲存格式,支援壓縮、列存、行存、分散式計算、大平行計算,適合持久儲存大量資料,並進行高效能運算。
在資料庫外的結構化資料計算方面,Stream做出了突破性的貢獻;Kotlin加強了這種能力,但編譯性語言的特性使它無法走得更遠;要想徹底解決庫外計算的難題,還需要SPL這種專業的結構化資料計算語言。
歡迎對SPL有興趣的加小助手(VX號:SPL-helper),進SPL技術交流群