Stream 和 Kotlin 能在 Java 中取代 SQL 吗

SQL计算能力较强,在JAVA开发中广泛应用于结构化数据计算,但SQL深度绑定数据库,存在架构性缺陷,包括计算代码难以移植、业务逻辑不支持热切换、计算性能无法低成本扩展等。现代应用架构更推崇在在JAVA中直接实现业务逻辑,数据库仅仅用于持久化存储,这样就需要实现数据库外的结构化数据计算。

JAVA早期没有提供相关类库,只能硬写代码,非常困难。后来JAVA推出了Stream库以及基于它进一步发展出来Kotlin,凭借Lambda表达式、流式编程风格、集合函数,才让结构化数据的计算处理变得方便了很多。经过多个版本的迭代,Stream/Kotlin已经广泛应用于各类项目,似乎有取代SQL实现计算的势头,这样就可以将SQL的作用仅限于数据存取,从而实现期望的架构。

Stream迈出了库外结构化数据计算的第一步

当数据对象是简单数据类型时(整数、浮点、字符串、日期),Stream可方便地实现集合计算,比如:

//过滤:
IntStream iStream=IntStream.of(1,3,5,2,3,6);
IntStream r1=iStream.filter(m->m>2);
//排序:
Stream r2=iStream.boxed().sorted();
//汇总:
int r3=iStream.sum();

相对直接写Java,上面的代码要简短得多,这说明Stream确实具有一定的计算能力。其它像去重distinct \合并concat \包含contain等常规运算也可以简单地完成。

Stream计算能力还无法取代SQL

当数据对象是简单数据类型时,Stream的确比较方便了,但结构化计算的数据对象不是简单数据类型,而是记录。一旦数据对象变成记录,Stream就不那么方便了。

比如排序:

Stream<Order> result=Orders
.sorted((sAmount1,sAmount2)->Double.compare(sAmount1.Amount,sAmount2.Amount))
.sorted((sClient1,sClient2)->CharSequence.compare(sClient2.Client,sClient1.Client));

等价的SQLselect * from Orders order by Client desc, Amount

可以看出,SQL排序时只要知道字段名,Stream则要额外给出字段类型,不如SQL简单。SQL用符号asc/desc就能表示顺序逆序,Stream则要用compare函数指明,代码繁琐。另外,SQL写出的字段顺序和要排序的字段顺序一致,Stream必须反着写,不如SQL直观自然。

再比如分组汇总:

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());
    }

等价的SQLselect year(OrderDate), sellerid, sum(Amount), count(1) from Orders group by year(OrderDate), sellerid

对比可知,同样使用Lambda表达式,SQL非常简短,Stream代码则要写一大段。同样是分组的动作,SQL的表达式直观易懂;Stream要用嵌套的Lambda表达式,难以解读。不影响上下文理解的情况下,SQL可以省略表名,直接用字段名;Stream则必须带上表名,即"表名.字段名",语法的设计不够巧妙。SQLgroup by进行分组,用sum求和,没有多余的内容;Stream分组用groupingBycollectCollectors,求和用summarizingDoubleDoubleSummaryStatistics,每个动作都要用多个函数,编码量还是很大。

如果继续考察集合、关联等更多的计算,也会发现同样的规律:Stream实现计算的简洁性无法取代SQL,难以实现现代应用架构。

Stream能力不足的原因在于其宿主语言JAVA是编译型语言,无法提供专业的结构化数据对象,缺少来自底层的有力支持。编译型语言的返回值的结构必须事先定义,遇到较多的中间步骤时,就要定义多个数据结构,这不仅让代码变得繁琐,还导致参数处理不灵活,要用一套复杂的规则来实现Lambda语法。SQL是解释性语言,天然支持动态结构,可以将参数表达式指定为值参数或函数参数,提供更简单的Lambda语法。

KotlinStream进行改进,增强了计算能力

Kotlin是基于JAVA的现代开发语言,重点体现在对JAVA语法尤其是Stream的改进上,即Lambda表达式更加简洁,集合函数更加丰富,另外增加了热情集合计算(Eager Evaluation,与Stream的惰性集合计算相对)。

比如简单集合的交集:

var numbers=listOf(3,11,21,27,9)
var others=listOf(2,11,21)
var result=numbers intersect others

对比前面的例子可知,Stream不支持交集,而Kotlin的集合函数更丰富,支持简单集合的交集。Stream函数都是传统的前缀函数,计算时不够直观,Kotlin则增加了类似SQL的中缀函数,比如代码中的Intersect,更加简单直观。Stream必须将普通集合(List)转为惰性集合(Stream)才能进行计算,复用时也很麻烦,Kotlin可直接对热情集合进行计算,在输入输出、复用、类型转换时更方便。

同样的记录排序:

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

上面代码中,Kotlin直接对字段进行排序,不必像Stream那样额外指定类型。直接用函数表示顺序/逆序,不必像Stream那样硬编码。直接用it作为Lambda表达式的默认参数,不必像Steam额外定义。整体代码明显比Stream简短,计算能力更强。

Kotlin计算能力仍然无法取代SQL

Kotlin的确对Kotlin进行了改进,但Lambda表达式的规则仍然复杂,函数的数量和功能仍然不够,整体计算能力并没有显著增强,还是远不如SQL

以记录排序为例,Kotlin虽然提供了it这个默认参数(表名),但SQL里根本用不到表名,只要给出字段名就够了。Kotlin的排序函数只能对一个字段进行排序,如果要对多个字段排序,就要多次调用函数,而SQL的排序函数可以动态接收多个字段,只须调用一次函数。

同样的分组汇总:

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区别不大。用到字段的地方仍然要带上表名,语法设计和Stream一样不方便。Kotlin分组汇总的结果不是结构化数据类型,为进一步计算制造了麻烦,这和Stream一样。SQL支持动态数据结构,不必事先定义中间结果,Kotlin要事先定义中间结果的数据结构,比如对两个字段分组,就要事先定义双字段的数据结构,这还是和Stream没区别。

继续考察关联、归并等运算,就会发现Kotlin代码的确比Stream短一些,但该有的步骤一个不少,简洁性比SQL还是差得远,达不到取代SQL,实现现代应用架构的地步。

Kotlin计算能力不足,根本原因在于它和JAVA一样是编译型语言,不支持动态数据结构,无法提供专业的结构化数据对象。缺乏专业的结构化数据对象,就难以真正简化Lambda语法,无法脱离表名直接引用字段(比如写成“单价*数量”无法直接动态支持多个字段的计算(比如多字段排序),要辅以复杂的编码才能完成大多数计算。

要想在JAVA应用中取代SQL实现计算以获得期望的架构优势,esProc SPL才是更好的选择。

SPL具有足够的计算能力,可以取代SQL

esProc SPLJVM下的开源结构化数据计算语言,提供了专业的结构化数据类型,内置丰富的计算函数,以及大量日期和字符串函数,支持SQL语法,提供了方便的JDBC接口,具有足够的计算能力,可以在JAVA中取代SQL

专业的结构化数据类型

业务逻辑围绕结构化数据对象展开,Stream/Kotlin的结构化数据对象不支持动态数据结构,专业性不足。类似SQLSPL的结构化数据对象支持动态数据结构,专业性更强。

取记录的字段值:=r.AMOUNT*0.05

修改记录的字段值:=r.AMOUNT= T.AMOUNT*1.05

序表取一列:T.(AMOUNT)

序表追加记录:T.insert(0,31,"APPL",10,2400.4)

按字段名取一列,返回简单集合:T.(AMOUNT)

取几列,返回集合的集合:T.([CLIENT,AMOUNT])

取几列,返回新序表:T.new(CLIENT,AMOUNT)

先按字段取再按记录序号取:T.(AMOUNT)(2);等价于先按记录序号取再按字段取:T(2).AMOUNT

除此之外,SPL序表还支持很多高级功能,如TopN、蛇形取值、有序关联等。

SPL内置丰富的计算函数,基础计算无须简单方便

Stream/Kotlin的计算函数数量少,功能弱,需要辅以大量编码,SPL内置丰富的计算函数,基础计算一句搞定。

比如排序:=Orders.sort(-Client, Amount)

SQL类似,SPL使用字段时无须附带表名,无须指明排序字段的数据类型,无须使用函数来表达顺序/逆序,使用字段时无须附带表名,一个函数就可以动态地对多个字段进行排序。

分组汇总:=Orders.groups(year(OrderDate),Client; sum(Amount),count(1))

SQL类似,SPL整体代码没有多余的函数,sumcount用法简洁易懂,甚至很难觉察这是嵌套的匿名函数。对双字段进行分组或汇总时,SPL不需要事先定义数据结构。分组汇总的结果仍然是结构化数据对象,可以直接参与下一步计算。

由于可以用变量代表中间计算结果,实际上SPL的多步骤计算比SQL更方便。比如,分别算出两个子集后,对子集进行交集运算:

=Orders1 ^ Orders2

上面代码中的^是中缀运算符,表示交集,比KotlinSQL的中缀函数更方便。类似的集合运算符还有并集&、差集\,以及专门的合集运算符|

考察更多基础计算,SPL同样简单:

去重:=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还支持常见的SQL语法

//filter
$select * from d:/sOrder.xlsx where Client like '%S%' or (Amount>1000 and Amount<=2000)
//sort
$select * from sales.xls order by Client,Amont desc
//distinct
$ select distinct(sellerid) from sales.xls        
//group by…having
$select year(orderdate) y,sum(amount) s from sales.xls group by year(orderdate) having sum(amount)>=2000000
//join
$select e.name, s.orderdate, s.amount from sales.xls  s left join employee.csv e on s.sellerid= e.eid

SPL支持SQL-92标准中大部分语法,包括集合计算、case whenwith、嵌套子查询等,详见《没有 RDB 也敢揽 SQL 活的开源金刚钻 SPL

丰富的日期和字符串函数

SPLStream/Kotlin支持更多的日期函数和字符串函数,在数量和功能上甚至超过了SQL,同样的计算代码量更短。比如,时间类函数,季度增减:

elapse("1980-02-27",-3) //返回1979-05-27

星期几:day@w("2020-02-27") //返回5,即星期4

N个工作日之后的日期:workday(date("2022-01-01"),25) //返回2022-02-04

字符串类函数,判断是否全为数字:isdigit("12345") //返回true

取子串前面的字符串:substr@l("abCDcdef","cd") //返回abCD

按竖线拆成字符串数组:"aa|bb|cc".split("|") //返回["aa","bb","cc"]

SPL还支持年份增减、求年中第几天、求季度、按正则表达式拆分字符串、拆出SQLwhereselect部分、拆出单词、按标记拆HTML等功能。

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\").groups(year(OrderDate),Client; sum(Amount))";
ResultSet result = statement.executeQuery(str);

SPL的计算能力和易集成性,使得现代应用架构更易于实现。

SPL具有超越SQL的计算能力

SQL缺乏序号,集合化不彻底,相关的计算难以实现。SPL天然支持序号,集合化更彻底,具有强大的有序和集合计算能力。SQL缺乏流程控制能力,难以实现复杂的业务逻辑,SPL具有灵活方便的流程控制能力,适合简化复杂的业务逻辑。

有序和集合运算

SQL缺乏序号,部分数据库虽有相关的伪字段,但用法古怪,比如查询序号10-20之间的记录,必须写成子查询的形式:

select * from (select rownum no,id,name from student where rownum<=20) where no >10

SPL天然支持序号,相应的查询直观方便:

student.select(#>10 && #<=20)

再比如,按列号取几列,返回新序表:T.new(#2,#4)

按序号倒数取记录:T.m(-2)

按序号取某几条记录形成序表:T([3,4,5])

按范围取记录形成序表:T(to(3,5))

涉及跨行的有序运算,通常都有一定的难度,SQL缺乏序号,只能用关联或窗口函数实现跨行,简单些的还能应付,比如比上期和同期比,但复杂些的就麻烦多了,比如:计算某支股票最长的连续上涨天数,SQL写出来很复杂:

计算某支股票最长的连续上涨天数,SQL 写出来很复杂:
select max(days_of_continuous) 
from (select count(*) days_of_continuous
      from (select sum(sign_of_updown) over(order by transDate) days_of_not_up
            from (select transDate,
                      case when
                          price>LAG(price) over(order by transDate)
                     then 0 else 1 end sign_of_updown
                  from share) )
group by days_of_not_up)

SPL可以用相对位置引用字段,实现跨行计算简单得多,还能自动处理数组越界等特殊情况。对于上面同样的计算,代码要简单很多:


A

1

=orcl.query@x(select price from stock order by transDate)

2

=t=0,A1.max(t=if(price>price[-1],t+1,0))

SQL集合化不彻底,无法保持中间集合,必须一次性计算出结果,导致代码繁琐难懂。SPL支持彻底的集合化,可以用变量保持中间集合,再分步进行后续计算,有助于理清思路并简化代码。比如,找出公司中与其他人生日相同的员工,可以在分组后保持子集参与进一步计算:


A

1

=demo.query(“select * from emp”).group(month(birthday),day(birthday))

2

=A1.select(~.len()>1).conj()

流程控制能力

业务逻辑经常涉及较多流程控制,SQL本身缺乏流程控制能力,需要借助存储过程,但存储过程与数据库耦合更深,且更难移植,架构变得更差。SPL提供了完整的流程控制能力,适合简化复杂的业务逻辑。分支判断语句:


A

B

2


3

if T.AMOUNT>10000

=T.BONUS=T.AMOUNT*0.05

4

else if T.AMOUNT>=5000 && T.AMOUNT<10000

=T.BONUS=T.AMOUNT*0.03

5

else if T.AMOUNT>=2000 && T.AMOUNT<5000

=T.BONUS=T.AMOUNT*0.02

循环语句:


A

B

1

=db=connect("db")


2

=T=db.query@x("select * from sales where SellerID=? order by OrderDate",9)

3

for T

=A3.BONUS=A3.BONUS+A3.AMOUNT*0.01

4


=A3.CLIENT=CONCAT(LEFT(A3.CLIENT,4), "co.,ltd.")

5


 …

SPL还可用break关键字跳出(中断)当前循环体,或用next关键字跳过(忽略)本轮循环,不展开说了。

SPL具有强大的有序和集合计算能力、灵活方便的流程控制能力,可以独立实现复杂的业务逻辑,使SQL切实地专注于数据存取。

SPL支持更优的应用架构

除了计算能力,SPL在系统架构、数据源、中间数据存储、计算性能上也有一些特有的优势,有助于SPL实现更优的应用架构。

解释执行和热切换

前面例子中,简单的SPL代码可嵌入JAVA代码,如果代码较复杂或频繁修改,也可以保存为脚本文件,外置于JAVA,通过文件名被调用。SPL是解释型语言,修改后可直接运行,无须编译,不必重启JAVA服务。比如:

Class.forName("com.esproc.jdbc.InternalDriver");
Connection connection =DriverManager.getConnection("jdbc:esproc:local://");
Statement statement = connection.createStatement();
ResultSet result = statement.executeQuery("call runSub()");

SPL计算代码外置于JAVA,可有效降低系统耦合性。

SPL支持多种数据源,可进行跨源计算和跨库计算。

以往的应用架构中,要计算非RDB数据源,或进行非RDBRDB的混合计算时,通常要先将非RDB数据源导入RDB,再进行计算,这种架构额外增加了计算模块和数据库的耦合性,代码也难以移植。SPL可直接计算各类数据源,支持非RDBRDB的混合计算,不仅可以解除计算模块和数据库的耦合性,还能使不同的数据源获得一致的计算能力,并使代码在不同数据源之间无缝移植。

SPL支持数据库,txt\csv\xls等文件,MongoDBHadoopredisElasticSearchKafkaCassandraNoSQL,特别地,还支持WebService XMLRestful 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


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存储格式,支持压缩、列存、行存、分布式计算、大并发计算,适合持久存储大量数据,并进行高性能计算。

StreamKotlin基于编译型语言,结构化数据类型不专业,计算能力不足,还难以在JAVA中取代SQL的计算能力。SPL提供了专业的结构化数据类型,有足够能力取代SQL的计算,而且易于被Java集成,这些特性有助于现代应用架构的实现,使业务逻辑保持在应用程序中,而SQL只负责数据库读写。不仅如此,SPL还提供了强大的有序和集合运算能力,以及流程控制能力,具有全面超越SQL的计算能力,让SQL切实地专注于数据读写。而且,SPL还支持热切换、多种数据源、自有存储格式以及高性能计算,可进一步优化应用架构。

以下是广告时间

对润乾产品感兴趣的小伙伴,一定要知道软件还能这样卖哟性价比还不过瘾? 欢迎加入好多乾计划。
这里可以低价购买软件产品,让已经亲民的价格更加便宜!
这里可以销售产品获取佣金,赚满钱包成为土豪不再是梦!
这里还可以推荐分享抢红包,每次都是好几块钱的巨款哟!
来吧,现在就加入,拿起手机扫码,开始乾包之旅



嗯,还不太了解好多乾?
猛戳这里
玩转好多乾