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));
等价的SQL:select * 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());
}
等价的SQL:select year(OrderDate), sellerid, sum(Amount), count(1) from Orders group by year(OrderDate), sellerid
对比可知,同样使用Lambda表达式,SQL非常简短,Stream代码则要写一大段。同样是分组的动作,SQL的表达式直观易懂;Stream要用嵌套的Lambda表达式,难以解读。不影响上下文理解的情况下,SQL可以省略表名,直接用字段名;Stream则必须带上表名,即"表名.字段名",语法的设计不够巧妙。SQL用group by进行分组,用sum求和,没有多余的内容;Stream分组用groupingBy、collect、Collectors,求和用summarizingDouble、DoubleSummaryStatistics,每个动作都要用多个函数,编码量还是很大。
如果继续考察集合、关联等更多的计算,也会发现同样的规律:Stream实现计算的简洁性无法取代SQL,难以实现现代应用架构。
Stream能力不足的原因在于其宿主语言JAVA是编译型语言,无法提供专业的结构化数据对象,缺少来自底层的有力支持。编译型语言的返回值的结构必须事先定义,遇到较多的中间步骤时,就要定义多个数据结构,这不仅让代码变得繁琐,还导致参数处理不灵活,要用一套复杂的规则来实现Lambda语法。SQL是解释性语言,天然支持动态结构,可以将参数表达式指定为值参数或函数参数,提供更简单的Lambda语法。
Kotlin对Stream进行改进,增强了计算能力
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 SPL是JVM下的开源结构化数据计算语言,提供了专业的结构化数据类型,内置丰富的计算函数,以及大量日期和字符串函数,支持SQL语法,提供了方便的JDBC接口,具有足够的计算能力,可以在JAVA中取代SQL。
专业的结构化数据类型
业务逻辑围绕结构化数据对象展开,Stream/Kotlin的结构化数据对象不支持动态数据结构,专业性不足。类似SQL,SPL的结构化数据对象支持动态数据结构,专业性更强。
取记录的字段值:=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整体代码没有多余的函数,sum和count用法简洁易懂,甚至很难觉察这是嵌套的匿名函数。对双字段进行分组或汇总时,SPL不需要事先定义数据结构。分组汇总的结果仍然是结构化数据对象,可以直接参与下一步计算。
由于可以用变量代表中间计算结果,实际上SPL的多步骤计算比SQL更方便。比如,分别算出两个子集后,对子集进行交集运算:
=Orders1 ^ Orders2
上面代码中的^是中缀运算符,表示交集,比Kotlin和SQL的中缀函数更方便。类似的集合运算符还有并集&、差集\,以及专门的合集运算符|。
考察更多基础计算,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 when、with、嵌套子查询等,详见《没有 RDB 也敢揽 SQL 活的开源金刚钻 SPL》
丰富的日期和字符串函数
SPL比Stream/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还支持年份增减、求年中第几天、求季度、按正则表达式拆分字符串、拆出SQL的where或select部分、拆出单词、按标记拆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数据源,或进行非RDB和RDB的混合计算时,通常要先将非RDB数据源导入RDB,再进行计算,这种架构额外增加了计算模块和数据库的耦合性,代码也难以移植。SPL可直接计算各类数据源,支持非RDB和RDB的混合计算,不仅可以解除计算模块和数据库的耦合性,还能使不同的数据源获得一致的计算能力,并使代码在不同数据源之间无缝移植。
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:
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基于编译型语言,结构化数据类型不专业,计算能力不足,还难以在JAVA中取代SQL的计算能力。SPL提供了专业的结构化数据类型,有足够能力取代SQL的计算,而且易于被Java集成,这些特性有助于现代应用架构的实现,使业务逻辑保持在应用程序中,而SQL只负责数据库读写。不仅如此,SPL还提供了强大的有序和集合运算能力,以及流程控制能力,具有全面超越SQL的计算能力,让SQL切实地专注于数据读写。而且,SPL还支持热切换、多种数据源、自有存储格式以及高性能计算,可进一步优化应用架构。
对润乾产品感兴趣的小伙伴,一定要知道软件还能这样卖哟性价比还不过瘾? 欢迎加入好多乾计划。
这里可以低价购买软件产品,让已经亲民的价格更加便宜!
这里可以销售产品获取佣金,赚满钱包成为土豪不再是梦!
这里还可以推荐分享抢红包,每次都是好几块钱的巨款哟!
来吧,现在就加入,拿起手机扫码,开始乾包之旅
嗯,还不太了解好多乾?
英文版