流式結構化數據計算語言的進化與新選擇

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技術交流羣

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