SQLite 的挑战者 esProc SPL
很多小微型应用程序也需要一些数据处理和计算能力,如果集成一个数据库就显得太沉重了,这种情况下 SQLite 是一个不错的选择,它架构简单,集成方便,可持久化存储数据,并提供 SQL 实现计算能力。
但是,对于某些较复杂的场景,SQLite 仍有不足之处。
SQLite面对复杂场景的不足
数据源支持
SQLite 像个数据库,可以对自有的库文件提供较好的支持,但应用程序有时还要处理其它形式的数据,比如文本文件、Excel、其它数据库、Restful 等 Web 上的数据。SQLite 只支持 csv 文件的读取,不支持其他数据源,除非硬编码。而且,SQLite 虽然支持 csv 文件,但使用过程很繁琐,需要先用命令行创建数据库,再用 create 命令创建表结构,然后用 import 命令导入数据,最后才能用 SQL 查询数据。
除了常规结构化数据,现代应用还会经常碰到 Json,XML 等复杂格式的数据。SQLite 有计算 Json 串的能力,但不支持直接读取多层数据源,包括 Json 文件 /RESTful,需要硬写代码,或再借助第三方类库,拼成 insert 语句插入数据表,代码很繁琐。SQLite 也不能计算 XML 串,更不能读取 XML 文件 /WebService。
应用程序有时需要把数据写成格式通用的文件,以便输出、转移或交换,有时候还要把数据主动写入其他数据源。但 SQLite 只能将数据持久化到自有的库文件,不能直接写入外部数据源,包括基本的 csv 文件。
复杂计算
SQLite 采用 SQL 语句做计算,SQL 的优点和缺点都会继承下来。SQL 接近自然语言,学习门槛低,容易实现简单的计算,但不擅长复杂的计算,经常会造成代码会繁琐难懂。
即使一些不太复杂的计算,SQL 实现起来也不容易。比如,计算每个客户销售额最大的 3 笔订单:
select * from (select *, row_number() over (partition by Client order by Amount desc) as row_number from Orders) where row_number<=3
这个例子要计算组内的前 N 条记录,需要用窗口函数生成组内的序号伪列,再过滤伪列,代码因而显得复杂。
复杂些的计算,SQL 代码更加冗长难懂。比如,某支股票的最大连续上涨天数:
select max(continuousdays)
from (
select count(*) continuousdays
from (
select sum(risingflag) over (order by day) norisingdays
from (
select day, case when price>lag(price) over (order by day) then 0 else 1 end risingflag
from tbl
)
) group by norisingdays
)
SQL 很难直接表达连续上涨的概念,只能换个方法变相实现,即通过累计不涨天数来计算连续上涨天数,这种方法技巧性强,编写难度大且不易理解。而且 SQL 难以调试,导致维护困难。
再看个例子:找出销售额占到一半的前 n 个客户,并按销售额从大到小排序。
with A as
(select client,amount,row_number() over (order by amount) ranknumber
from sales)
select client,amount
from (select client,amount,sum(amount) over (order by ranknumber) acc
from A)
where acc>(select sum(amount)/2 from sales)
order by amount des
SQL 很难处理恰好要过线的客户,只能换个方法变相实现,即计算销售额从小到大的累计值,反过来找出累计值不在后一半的客户。这种方法技巧性强,代码冗长,而且难以调试。
除此之外,SQLite 的日期和字符串函数也不够丰富,比如缺乏季度增减、工作日计算等,这些缺点限制了 SQLite,不适合计算需求较复杂的场景。
流程处理
SQL 本身缺乏流程处理能力,数据库会借助存储过程实现完整的业务逻辑,但 SQLite 不支持存储过程,也就无法直接实现完整的业务逻辑,只能借助主应用的能力,将 SQL 数据对象转为应用中的数据对象(比如 Java 的 resultSet/List<EntityBean> 等对象),再用主程序的 for/if 等语句处理流程,最后再转回 SQL 的数据对象,代码非常繁琐。复杂的业务逻辑要在 SQL 对象和主应用的对象之间转换多次,更加麻烦,这里就不展示了。
esProc SPL解决 SQLite 困难
如果要为 Java 小微型应用提供数据处理和计算能力,还有一个更好的选择:esProc SPL。
esProc SPL 是 Java 下开源的数据处理引擎,架构简单,集成方便,可持久化存储数据,有足够的计算能力,这些特点与 SQLite 类似。
SPL 架构简单,不用配置服务、节点、集群,只要引入 SPL 的 Jar 包,就可以部署在 Java 环境中。
SPL 提供了 JDBC 接口,可以方便地集成到 Java 应用中,简单的查询类似 SQL。
Class.forName("com.esproc.jdbc.InternalDriver");
Connection conn =DriverManager.getConnection("jdbc:esproc:local://");
Statement statement = conn.createStatement();
ResultSet result = statement.executeQuery("=T(\"D:/Orders.csv\").select(Amount>1000 && like(Client,\"*s*\"))");
SPL 支持数据持久化,可以将数据保存到自有数据格式(集文件)中,比如批量新增记录:
A |
|
1 |
=create(OrderID,Client,SellerID,Amount,OrderDate) |
2 |
=A1.record([201,"HDR",9,2100.0,date("2021-01-01"), 203,"APPLE",4,1900,date("2021-01-03")]) |
3 |
=file("d:/Orders.btx").export@ab(A2) |
上面 A3 代码 export@ab,@a 表示追加,@b 表示集文件格式
除了直接持久化,也可以先处理内存中的序表(SPL 的结构化数据对象,可类比为 SQL 结果集),再将序表覆盖写入集文件,具体做法是将 export@ab 改为 export@b。这种方式性能不如 SQLite,但小微型应用的数据量普遍不大,覆写的速度通常可接受。
组表是 SPL 的另一种自有数据格式,支持高性能批量增删改,适用于大数据量高性能计算(这不是本文重点)。
除了自有格式,SPL 也可以将数据保存到 csv 文件中,只要把 A3 改为:
file("d:/Orders.csv").export@tc(A2)
SPL 有足够的计算能力,支持各类 SQL 式计算,包括分组后计算(窗口函数):
A |
B |
|
1 |
=Orders.new(Client,Amount) |
// 选出部分字段 |
2 |
=Orders.select(Amount>1000 && like(Client,\"*s*\")) |
// 模糊查询 |
3 |
= Orders.sort(Client,-Amount) |
// 排序 |
4 |
= Orders.id(Client) |
// 去重 |
5 |
=Orders.groups(year(OrderDate):y,Client;sum(Amount):amt).select(amt>3000) |
// 分组汇总 |
6 |
=[Orders.select(Amount>3000),A1.select(year(OrderDate)==2009)].union() |
// 并集 |
7 |
=Orders.groups(year(OrderDate):y,Client;sum(Amount):amt).select(like(Client,\"*s*\")) |
// 子查询 |
8 |
=A5.derive(amt/amt[-1]-1: rate) |
// 跨行 |
SPL 提供了基本的 SQL 语法,比如分组汇总:
$select year(OrderDate) y,month(OrderDate) m, sum(Amount) s,count(1) c from {Orders}
Where Amount>=? and Amount<? ;arg1,arg2
除了这些基础能力外,SPL 还能克服了 SQLite 的各种不足,全面支持各类数据源,具有更强的计算能力,流程处理方便,可以面对更复杂的应用场景。
数据源支持
SPL 读取 csv 文件只需一步,在 Java 里嵌入下面的 SPL 代码:T("d:/Orders.csv").select(Amount>2000 && Amount<=3000)
函数 T 不仅可以读取集文件,也可以读取 csv 文件,并生成序表。SPL 导入数据时,数据类型会自动解析,不必手工指定。整个过程无需多余编码,比 SQLite 方便多了。
如果 csv 格式不规范,还可以使用 import 函数指定分隔符、字段类型、跳过行数,并处理转义符、引号、括号等,比 SQLite 提供的功能丰富多了。
SPL 内置多种数据源接口,包括 tsv、xls、Json、XML、RESTful、WebService,以及其他数据库,甚至支持 Elasticsearch、MongoDB 等特殊数据源。
这些数据源都可以直接使用,非常方便。对于其他未列入的数据源,SPL 也提供了接口规范,只要按规范输出为 SPL 的结构化数据对象,就可以进行后续计算。
SPL 可直接解析多层数据源。读取并计算 Json 文件:
json(file("d:/xml/emp_orders.json").read()).select(Amount>2000 && Amount<=3000)
json(httpfile("http://127.0.0.1:6868/api/orders").read()).select(Amount>2000 && Amount<=3000)
XML 文件:
A |
|
1 |
=file("d:/xml/emp_orders.xml").read() |
2 |
=xml(A1,"xml/row") |
3 |
=A2.select(Amount>1000 && Amount<=2000 && like@c(Client,"*business*")) |
WebService:
A |
|
1 |
=ws_client("http://127.0.0.1:6868/ws/RQWebService.asmx?wsdl") |
2 |
=ws_call(A1,"RQWebService":"RQWebServiceSoap":"getEmp_orders") |
3 |
=A2.select(Amount>1000 && Amount<=2000 && like@c(Client,"*business*")) |
SPL 序表支持多层结构数据,比 SQL 库表的二维结构更容易表达 Json/XML,计算代码也更简单。这部分内容不是本文重点,就此略过。
跨源计算
SPL 开放性较好,可以直接计算多种数据源,这些数据源可以和 SPL 集文件进行跨源计算。比如,对集文件和 csv 进行内关联分组汇总:
join(T("d:/Orders.btx"):o,SellerId; T("d:/Emp.csv"):e,EId).groups(e.Dept;sum(o.Amont))
外部数据源之间也可以方便地进行跨源计算。比如 csv 和 RESTful 左关联:
join@1(json(httpfile("http://127.0.0.1:6868/api/orders").read()):o,SellerId; T("d:/Emp.csv"):e,EId)
写成多步骤的形式更易读:
A |
|
1 |
=Orders=json(httpfile("http://127.0.0.1:6868/api/orders").read()) |
2 |
=Employees=T("d:/Emp.csv") |
3 |
=join@1(Orders:o,SellerId;Employees:e,EId) |
只用 SPL 语言就可以实现跨源计算,不必借助 Java 或命令行,代码简短易懂,比 SQL 的开发效率高得多。
任意数据源的持久化
SPL 除了支持自有数据格式的持久化,也支持其他数据源,同样是通过序表为媒介。比如:
file("d:/Orders.csv").export@t(A2) //csv文件
file("d:/Orders.xlsx").xlsexport@t(A2) //xls文件
file("d:/Orders.json").write(json(A2)) //json文件
特别地,SPL 支持任意数据库的持久化,以 Oracle 为例:
A |
B |
|
1 |
=connect("orcl") |
/连接外部 oracle |
2 |
=T=A1.query("select * from salesR where SellerID=?",10) |
/批量查询,序表 T |
3 |
=NT=T.derive() |
/复制出新序表 NT |
4 |
=NT.field("SELLERID",9) |
/批量修改新序表 |
5 |
=A1.update(NT:T,sales;ORDERID) |
/持久化 |
数据库的持久化以序表为媒介,其优点相当明显:函数 update 可自动比对修改(增改删)前后的序表,能够方便地实现批量数据地持久化。
计算能力
SPL 支持有序计算、集合计算、分步计算、关联计算,可以简化复杂的结构化数据计算。
简单的比如,计算每个客户销售额最大的 3 笔订单:
Orders.group(Client).(~.top(3;Amount))
SPL 代码很直观,先按 Client 分组,再对各组(即符号 ~)计算 TopN。SPL 之所以代码简单,表面上是因为 SQL 没有 top 函数而 SPL 直接提供了,本质是因为 SPL 有真正的行号字段,或者说,SPL 支持有序集合。SPL 代码简单,还因为集合化更加彻底,可以实现真正的分组,即只分组不汇总,这就可以直观地计算组内数据。
复杂些的计算,SPL 实现起来也不难。最大连续上涨天数:
A |
|
1 |
=tbl.sort(day) |
2 |
=t=0,A1.max(t=if(price>price[-1],t+1,0)) |
SPL 容易表达连续上涨的概念,先按日期排序;再遍历记录,发现上涨则计数器加 1。这里既用到了循环函数 max,也用到了有序集合,代码中 [-1] 表示上一条,是相对位置的表示方法,price[-1]表示上一个交易日的股价,比整体移行(lag 函数)更直观。
再看个例子,求销售额占到一半的前 n 个客户:
A |
B |
|
2 |
=sales.sort(amount:-1) |
/销售额逆序排序,可在SQL中完成 |
3 |
=A2.cumulate(amount) |
/计算累计序列 |
4 |
=A3.m(-1)/2 |
/最后的累计即总额 |
5 |
=A3.pselect(~>=A4) |
/超过一半的位置 |
6 |
=A2(to(A5)) |
/按位置取值 |
SPL 集合化成更彻底,可以用变量方便地表达集合,并在下一步用变量引用集合继续计算,因此特别适合多步骤计算。将大问题分解为多个小步骤,可以方便地实现复杂的计算目标,代码不仅简短,而且易于理解。此外,多步骤计算天然支持调试,无形中提高了开发效率。
上面例子使用了有序计算、集合计算、分步计算,SPL 从简单到复杂的计算都可以很好的完成。此外,SPL 还支持游离记录,可以用点号直观地引用关联表,从而简化复杂的关联计算。
SPL 还提供了更丰富的日期和字符串函数,在数量和功能上远远超过传统数据库。
值得一提的是,为了进一步提高开发效率,SPL 还创造了独特的函数语法。
流程处理
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 还有更多针对结构化数据的流程处理功能,可进一步提高开发效率,比如:每轮循环取一批而不是一条记录;某字段值变化时循环一轮。
上面的业务逻辑可保存为脚本文件,并置于应用程序外,以存储过程的形式被调用:
Class.forName("com.esproc.jdbc.InternalDriver");
Connection conn =DriverManager.getConnection("jdbc:esproc:local://");
CallableStatement statement = conn.prepareCall("{call queryOrders()}");
statement.execute();
SPL 是解释型代码,修改后不必编译就可直接运行,也不必重启应用,可有效降低维护成本。外置的 SPL 脚本不仅可以有效降低系统耦合性,还具有热切换的特点。SQLite 不支持存储过程,也就不能将业务逻辑外置于主应用,耦合性高,应用结构差。
SPL 在 Java 下明显优于 SQLite,但对于非 Java 应用就会麻烦一点了,只能使用独立的 ODBC 服务或 HTTP 服务的方式,架构不够轻便,集成性也下降了。需要注意的是,android 属于 Java 体系,SPL 可以正常运行,但 iOS 目前还没有较成熟的 JVM 环境,SPL 就无法支持了。
英文版