流式結構化資料計算語言的進化與新選擇

2022-08-11 09:00:36

JAVA開發中經常會遇到不方便使用資料庫,但又要進行結構化資料計算的場景。JAVA早期沒有提供相關類庫,即使排序、分組這種基本計算也要硬寫程式碼,開發效率很低。後來JAVA8推出了Stream庫,憑藉Lambda表示式、鏈式程式設計風格、集合函數,才終於解決了結構化資料計算類庫從無到有的問題。

Stream可以簡化結構化資料的計算

比如排序:

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函數連在一起用屬於鏈式程式設計風格,可以使多步驟計算變得直觀。

Stream計算能力還不夠強

仍然以上面的排序為例,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表示式更加簡潔,集合函數更加豐富。

Kotlin計算能力強於Stream

比如排序:

var resutl=Orders.sortedBy{it.Amount}.sortedByDescending{it.Client}

上面程式碼無須指明排序欄位的資料型別,無須用函數表達順序/逆序,直接參照it作為匿名函數的預設引數,而不是刻意定義,整體比Stream簡短不少。

Kotlin改進並不大,計算能力仍然不足

仍然以排序為例,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介面,擅長簡化複雜計算。

SPL內建豐富的計算函數實現基礎計算

比如排序:=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))

SPL提供了JDBC介面,可被JAVA程式碼無縫呼叫

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語法風格簡潔靈活,具有強大的計算能力。

SPL可簡化分步計算、有序計算、分組後計算等邏輯較複雜的計算,很多SQL/儲存過程難以實現的計算,用SPL解決起來就很輕鬆。比如,找出銷售額累計佔到一半的前n個大客戶,並按銷售額從大到小排序:


AB
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支援計算熱切換和程式碼外接,可降低系統耦合性。

比如,將上面的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支援多種資料來源,可進行跨源計算和跨庫計算。

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提供了自有儲存格式,可臨時或永久儲存資料,並進行高效能運算。

SPL支援btx儲存格式,適合暫存來自於低速資料來源的資料,比如CSV:


AB
1=[T("d:/orders1.csv"), T("d:/orders2.csv")].merge@u()/對記錄做並集
2file("d:/fast.btx").export@b(A1)/寫入集檔案

btx體積小,讀寫速度快,可以像普通文字檔案那樣進行計算:

=T("D:/fast.btx").sort(Client,- Amount)

如果對btx進行有序儲存,還能獲得高計算效能,比如平行計算、二分查詢。SPL還支援更高效能的ctx儲存格式,支援壓縮、列存、行存、分散式計算、大平行計算,適合持久儲存大量資料,並進行高效能運算。

在資料庫外的結構化資料計算方面,Stream做出了突破性的貢獻;Kotlin加強了這種能力,但編譯性語言的特性使它無法走得更遠;要想徹底解決庫外計算的難題,還需要SPL這種專業的結構化資料計算語言。

SPL資料

歡迎對SPL有興趣的加小助手(VX號:SPL-helper),進SPL技術交流群