从三个方面理解 SPL 是什么
几乎所有行业的核心数据都是结构化的,结构化数据是这个时代最重要的数据资产。那么如何利用处理好这些核心数据自然成了企业经营中的重中之重。当然,结构化数据处理技术也由来已久,SQL、Java、Python、Scala 都是经常用到的技术。
SPL(全称:Structured Process Language,结构化数据处理语言)就是一种新的结构化数据处理技术。
为什么已经有了这么多结构化数据处理技术还要再发明 SPL 呢?
那当然是因为当前技术在处理结构化数据时经常不如人意,而 SPL 的目标就是要弥补这些缺陷的同时能更进一步,让数据处理难度更低、效率更高。
我们主要从程序语言、计算中间件和数据仓库三个方面来一窥 SPL 的真容。
有趣的程序语言
作为专业的程序设计语言会有趣?这是什么情况?
SPL 的有趣来自于其独特的网格编程方式、语法特性和更完善的计算能力。
格子代码
跟几乎所有的编程语言采用文本方式不同,SPL 的代码是写在格子里的。SPL 的 IDE 是这个样子:
格子代码有一些不一样地方。首先是 SPL 编码时可以不用定义变量,后面步骤中可以直接使用前面单元格名(如 A1)就能使用该格的计算结果,这样就不用费心给变量起名字了;当然也支持定义变量。其次格子代码会非常整齐,即使某句代码很长也只会占一格,不影响整体结构,阅读更方便。还有就是 SPL 的 IDE 提供了多种调试方式,执行、调试执行、执行到光标等等,而且在 IDE 的右侧还有一个可以实时显示每个计算格子的结果面板,每步结果实时查看进一步增加了调试的便利。
语法特色
就语法本身来说,SPL 也有很多不一样的地方,包括选项语法、多层参数和增强 Lambda 语法。
每种编程语言都会提供很多函数,一些功能类似的函数还会使用同一个名称但不同参数(类型)进行区分,这种方式本身无可厚非。但有时通过参数类型也无法区分时就要显示地再增加选项参数来告诉编译器你要实现的功能,而选项也会作为参数,这就会导致混乱,搞不清这些参数的真实用途。
SPL 提供了非常独特的函数选项,使用 @符号来标识不同选项。比如 select 函数的基本功能是查找过滤,如果想从后往前查找可以使用 @z 选项:
T.select@z(Amount>1000)
如果想从第一个成员往后取,到第一个不满足条件的成员停止则可以使用 @c 选项:
T.select@c(Amount>1000)
两个选项还可以组合使用:
T.select@zc(Amount>1000)
有些函数的参数很复杂,可能会分成多层。常规程序语言对此并没有特别的语法方案,只能生成多层结构数据对象再传入,非常麻烦。
SPL 创造性地发明了层次参数。约定支持三层参数,分别用分号、逗号和冒号来分隔。分号是第一级,分号隔开的参数是一组,这个组内如果还有下一层参数则用逗号分隔,再下一层参数则用冒号分隔。
比如用 SPL 完成连接计算会非常简洁:
join(Orders:o,SellerId ; Employees:e,EId)
现在很多程序语言要么不支持 Lambda 语法,要么支持的很初级。还是拿 Java 来说,简单的计算用 Lambda 表达式确实方便,但稍复杂一点的写起来就费劲了,比如两个字段的分组汇总不仅需要事先定义数据结构,还会形成两个匿名函数的嵌套,代码难写难懂。
SPL 也提供了 Lambda 语法,并且相对 Java 这些语言的支持更加彻底,基于两个字段的分组汇总不需要事先定义结构更不会形成嵌套,简单易懂:
Orders.groups(year(OrderDate),Client; sum(Amount),count(1))
不仅如此,SPL 增加了~
和 #
以及 []
这些符号来分别表示当前成员、序号和相对位置,比如想引用上一条记录可以写成 [-1]。有了这些符号,不需要再增加参数定义就可以完成所有计算,描述能力变强了,书写上和理解上也更简单。
计算能力
相对 Java 这些缺少必要的结构化数据对象的语言,SPL 提供了专业的结构化数据对象序表,并在序表的基础上提供了丰富的计算类库。SPL 支持动态数据结构,这使得 SPL 具备了完善的结构化数据处理能力。
比如部分常规计算:
Orders.sort(Amount) // 排序
Orders.select(Amount*Quantity>3000 && like(Client,"*S*")) // 过滤
Orders.groups(Client; sum(Amount)) // 分组
Orders.id(Client) // 去重
join(Orders:o,SellerId ; Employees:e,EId) // 连接
这使得 SPL 具备了与 SQL 相当的能力,看起来 SPL 就像一个不依赖数据库的 SQL。其实远不止于此,SPL 对结构化数据的理解更加深刻,因此还有很多更强的方面。
比如对有序运算的支持 SPL 就更为直接和彻底。例如根据股票交易记录表计算:某支股票最长连续涨了多少交易日。用 SPL 很容易搞定:
stock.sort(trade_date).group@i(close_price<close_price [-1]).max(~.len())
但同样的计算用 SQL 就要嵌套 3 层,即使有窗口函数的辅助写起来仍然很绕,这都是因为对有序运算的支持不够导致的。
还有分组运算,SPL 可以保留分组子集,即集合的集合,这样可以很方便完成我们分组后对分组结果的进一步操作。相比之下,SQL 没有显示的集合数据类型,无法返回集合的集合这类数据,不能实现独立的分组,就只能强迫分组和聚合作为一个整体来计算了。
此外,SPL 对聚合运算也有新的理解,聚合结果除了常见的单值 SUM、COUNT、MAX、MIN 等之外,也可以是个集合。比如常常出现的 TOPN 计算,SPL 也看作和 SUM、COUNT 一样的聚合计算,既可以针对全集也可以针对分组子集。
其实 SPL 还有很多特性都是建立在结构化数据处理的深刻理解上,离散性可以让构成数据表的记录游离于数据表外独立反复使用;普遍集合支持任何数据构成的集合并参与运算;连接运算区分了三种不同连接类型可以因地制宜;而游标的支持让 SPL 拥有了大数据处理能力;……。有了这些特性以后,我们再处理数据就会更加简单、高效。
延伸阅读: 写在格子里的程序语言
强大的中间件
中间件的概念大家都不陌生,SPL 可以作为中间件专门用于数据计算( DCM:中间件家族迎来新成员 )。
从逻辑结构上看,SPL 可以介于数据源和应用之间,融合多源数据进行计算并为应用提供数据服务。这要求 SPL 具备了这样一些特点:
多源混算
SPL 具备开放的计算能力,可以对接多种数据源,RDB、NoSQL、CSV、Excel、JSON/XML、Hadoop、RESTful 都可以直接对接并进行混合计算,不需要入库,数据实时性和计算实时性都可以很好保障。
集成性
逻辑上 SPL 位于数据源和应用之间,物理上却可以和应用集成在一起。SPL 支持 jar 包嵌入式集成,集成后随应用一起部署十分灵活轻量。
敏捷性
从前面程序语言部分的介绍可以看到 SPL 具备良好的敏捷性,同样的计算使用 SPL 实现更为简洁,这使得作为计算中间件 SPL 可以快速响应多样的数据计算需求。
热切换
SPL 作为解释执行的程序语言天然支持不停机热切换热部署,对于一些稳定性差经常需要新增、修改计算逻辑的业务(如报表、微服务)非常友好。
高性能
SPL 内置了大量高性能算法,像前面提到的 TOPN 运算被看成聚合运算以后就不会进行大排序从而获得更高性能。还有索引、有序计算、并行计算等诸多机制可以进一步保证性能。
通过上面的特点,我们可以看出,现有技术中数据库(SQL)过于沉重和封闭不适合在中间做计算,也无法跨源尤其跨异构源计算;而 Java 在架构上虽然可以,但由于对结构化数据计算的支持不够导致很难用来充当中间件使用。
那么 SPL 作为中间件的应用场景有哪些呢?
便捷计算
应用中数据处理逻辑只能通过编码实现,使用原生的 Java 实现由于缺少必要的结构化计算类库往往比较困难,即使用新增加的 Stream/Kotlin 也并没有明显改善。借助 ORM 技术可以一定程度缓解开发困境,但仍然缺乏专业的结构化数据类型,集合运算不够方便,同时读写数据库时代码繁琐,复杂计算也难以实现。ORM 的这些缺点经常导致业务逻辑的开发效率不仅没有明显提升,甚至还大幅降低。此外,这些实现方式还会导致应用结构问题。Java 实现的计算逻辑必须与主应用一起部署导致紧耦合,同时由于不支持热部署开发运维也很麻烦。
借助 SPL 的敏捷计算、易集成、热切换等特性,在应用中替代 Java 实现数据处理逻辑,就可以很好解决上述问题,不仅开发效率提升,还可以优化应用结构,实现计算模块的解耦,同时支持热部署。
延伸阅读: Java 结构化数据处理开源库 SPL
多样性数据源计算
现代应用还经常面临多样性数据源问题,通过数据库处理不仅需要数据入库,效率低下,还无法保障数据的实时性。不同数据源有各自的优势,RDB 计算能力较强,但 IO 吞吐能力弱;NoSQL 的 IO 效率高,但计算能力很弱;而文本等文件数据完全没有计算能力,但使用非常灵活。强迫这些数据入库就会丧失这些原数据源的优势。
通过 SPL 的多源混算能力,不仅可以直接对 RDB、文本、Excel、JSON、XML、NoSQL 以及其他网络接口数据进行混合计算,保证数据与计算的实时性,而且还能同时保留各类数据源的优点,充分发挥其效力。
延伸阅读: 多数据源混合计算用什么技术?
微服务实现
当前微服务实现时仍然大量依赖 Java 和数据库实施数据处理,Java 的缺点在于实现复杂、无法热切换;而数据库由于有“库”的限制,多源数据要入库才能计算,灵活性很低,不仅数据时效性无法保证,也无法充分发挥各类数据源的优势。
将可集成的 SPL 分别嵌入微服务框架的各个环节完成数据采集整理、数据处理以及前置的数据计算任务,利用开放的计算体系可以充分发挥多数据源自身的优势,灵活性增强。多源数据处理、实时计算、热部署这些问题均能迎刃而解。
延伸阅读: 开源 SPL 令微服务真地”微“起来
存储过程替代
以往为了实现复杂计算或整理数据常常会使用存储过程,存储过程在库内计算有一定优势,但缺点也很明显。存储过程缺乏可移植性,编辑调试困难,创建和使用存储过程需要较高权限存在安全问题,为前端应用服务的存储过程还会造成数据库与应用紧耦合。
通过 SPL 将存储过程外置到应用中,可以实现“库外存储过程”,数据库则主要用于存储,将存储过程从数据库中解耦出来就可以很好解决存储过程带来的各类问题。
延伸阅读: 爱恨交加的存储过程该往何处去?
报表 BI 数据准备
为报表提供数据准备是 SPL 作为计算中间件的重要场景,以往使用数据库为报表准备数据存在实现难度高、耦合性强等问题,而报表本身计算能力不足又无法完成很多复杂计算。通过 SPL 的库外强计算能力就可以为报表提供一个专门的数据计算层,不仅可以解耦数据库为数据库减负,还可以弥补报表工具自身的计算能力不足。逻辑上分层后,报表开发维护都很清爽。
借助 SPL 将数据准备也工具化以后,结合报表工具就可以快速适应多变的报表需求,以更低成本应对没完没了的报表。
延伸阅读: 开源 SPL 优化报表应用应对没完没了
T+0 查询
数据量积累到一定程度时基于生产库查询会影响交易,这时就会将大量的历史数据剥离到其他历史数据库中,进行冷热数据分离。这时如果要查询全量数据就要完成跨库查询、冷热数据路由等工作。数据库对于跨库查询尤其是跨异构库存在很多问题,不仅效率低下,还存在数据传输不稳定、可扩展性低等很多不足,无法很好实现 T+0 全量数据查询。
而这些问题都可以通过 SPL 来解决,由于具备独立且完善的计算能力,可以分别从不同的数据库取数计算,因此可以很好适应异构数据库的情况,还可以根据数据库的资源状况决定计算是在数据库还是 SPL 中实施,非常灵活。在计算实现上,SPL 的敏捷计算能力还可以简化 T+0 查询中的复杂计算,提升开发效率。
延伸阅读: 开源 SPL 轻松应对 T+0
高效的数据仓库
SPL 不仅有计算,还有存储。有了计算和存储,SPL 就可以作为数据仓库使用。
可以看到,SPL 数据仓库与传统数据仓库在结构上基本一致,都以独立的服务器(可以横向扩展)方式运行,业务数据需要通过 ETL 固化到数据仓库,应用通过统一的 JDBC/Restful 接口访问 SPL 服务。
不过,SPL 与传统数据仓库还有很大不同。
使用 SPL 而非 SQL
SPL 数据仓库的形式化语言是 SPL,并没有使用业界普遍采用的 SQL。原因在前面的程序语言部分我们说过一些,这里我们再详细讲讲。简单来说,几十年前诞生的 SQL 并不能很好适应当代业务的需要,会面临:难写、跑不快和写不出的情况,而 SPL 可以解决这些问题。
开发效率
稍复杂的计算用 SQL 写起来很费劲,前面我们举过计算股票最长连涨天数的例子。这个计算用 SQL 写起来是这样:
select max(continuousDays) - 1
from (select count(*) continuousDays
from (select sum(changeSign) over(order by tradeDate) unRiseDays
from (select tradeDate,
case
when closePrice > lag(closePrice)
over(order by tradeDate) then
0
else
1
end changeSign
from stock))
group by unRiseDays)
这个 SQL 的细节这里不再解释,总体非常绕。
SPL 来写:
A |
|
1 |
=stock.sort(tradeDate) |
2 |
=0 |
3 |
=A1.max(A2=if(closePrice>closePrice[-1],A2+1,0)) |
在有序计算和过程化的支持下完全按照自然思维表达,简单明了。
前面给出过的写法:
stock.sort(trade_date).group@i(close_price<close_price [-1]).max(~.len())
是按上述 SQL 逻辑写的,因为有了有序计算的支持,也非常简单容易。
再比如根据销售记录计算每月前后各一个月的销售额移动平均值:
SQL:
WITH B AS
(SELECT LAG(Amount) OVER(ORDER BY Month) f1,
LEAD(Amount) OVER(ORDER BY Month) f2,
A.*
FROM Orders A)
SELECT Month,
Amount,
(NVL(f1, 0) + NVL(f2, 0) + Amount) /
(DECODE(f1, NULLl, 0, 1) + DECODE(f2, NULL, 0, 1) + 1) MA
FROM B
用 SPL 一句就搞定了:
Orders.sort(Month).derive(Amount[-1,1].avg()):MA)
写得简单意味着开发效率更高,运维也更简单,使用成本更低,这些都源于 SPL 对结构化数据计算的深入理解。
计算性能
写得复杂通常还会导致性能低下,反过来写得简单通常性能也高。我们知道数据仓库都有优化引擎,一个优秀的数据仓库优化引擎可以猜出一个查询语句的真正目标,从而采用更高效的算法执行(而不以字面表达的逻辑去执行)。比如 TOPN 计算时:
SELECT TOP 10 x FROM T ORDER BY x DESC
虽然 SQL 语句有排序字样,但大部分数据仓库都会优化,不会真排序。
但是改成组内 TOPN 以后:
select * from
(select y,*,row_number() over (partition by y order by x desc) rn from T)
where rn<=10
复杂度虽然没有提升很多,但很多数据库的优化引擎都会犯晕,猜不出这句 SQL 的真正目的,最后只能按照 SQL 表达的意思进行大排序,而大数据排序是非常慢的动作,最后导致性能低下。
其实这个计算还有不需要大排序的算法,但 SQL 无法表达,只能寄希望于优化器,但优化器面对复杂的情况又会犯晕没法使用更高性能的执行方式,这是传统数据仓库(SQL 技术)面临的困境。
SPL 在解决这类问题时首先会提供诸多高性能算法,像组内 TOPN,SPL 把这个计算也看成聚合运算(返回的不是单值而是集合),这样在工程实现时就可以避免全量数据的排序,从而获得更高性能。
SPL 提供了几十种这样的高性能算法来保证计算性能,包括:
-
内存计算类的二分法、序号定位、位置索引、哈希索引、多层序号定位、……
-
外存查找类的二分法、哈希索引、排序索引、带值索引、全文检索、……
-
遍历计算类的延迟游标、遍历复用、多路并行游标、有序分组汇总、序号分组、……
-
外键关联类的外键地址化、外键序号化、索引复用、对位序列、单边分堆、……
-
归并与连接类的有序归并、分段归并、关联定位、附表、……
-
多维分析类的部分预汇总、时间段预汇总、冗余排序、布尔维序列、标签位维度、……
-
集群计算类的集群复组表、复写维表、分段维表、冗余与备胎容错、负载均衡、……
这里提供的算法(有很多还是业界首创),针对不同的计算场景都有对应的机制保障,相对传统数据仓库更加丰富,足以保证计算性能。
在实际应用中,SPL 数据仓库也确实表现出与传统方案不一样的效果。比如,在某电商漏斗分析场景中 SPL 在使用更差硬件的情况下比 Snowflake 快了近 20 倍,而在国家天文台星体聚类计算场景下使用单台服务器比某头部互联网公司的分布式数据库还快了 2000 倍。类似的场景还有很多,基本上能够提速几倍到几十倍甚至上千倍,性能表现十分突出。
不过,相比传统数据仓库,SPL 没有做多少自动优化的功能,要跑出高性能,几乎全靠程序员写出低复杂度的代码(也就是这些内置函数的组合)。程序员需要经过一定的训练来熟悉 SPL 的理念和库函数,会多一个上手的门槛,不过获得数量级的性能提升和成倍的成本下降,这些付出通常也还是值得的。
技术栈
SQL 作为完备的计算语言理论上任何计算都能写得出,但如果场景太复杂工程很难实现就可以认为写不出来了,而这种情况并不少见。
比如电商用于分析客户流失率的漏斗分析场景用 SQL 就很难写,很多人自然认为这种涉及次序的多步骤运算就不适合 SQL 来做。
其实现在业界已经意识到 SQL 在处理复杂问题时的局限了,有一些数据仓库已经开始引入了 Python、Scala,以及应用 MapReduce 等技术来解决这类问题,但目前为止效果并不理想。MapReduce 性能太差,硬件资源消耗极高,而且代码编写非常繁琐,且仍然有很多难以实现的计算;Python 的 Pandas 在逻辑功能上还比较强,但细节上比较零乱,明显没有精心设计,有不少重复内容且风格不一致的地方,复杂逻辑描述仍然不容易;而且缺乏大数据计算能力以及相应的存储机制,也很难获得高性能;Scala 的 DataFrame 对象使用沉重,对有序运算支持的也不够好,计算时产生的大量记录复制动作导致性能较差,一定程度甚至可以说是倒退。而且这些风格完全迥异的技术会增加技术栈的复杂度。SQL 沉重臃肿的架构加上复杂的技术栈会大幅增加运维成本。
相比之下,SPL 的计算能力更加开放,提供了更全面的功能,很容易实现复杂计算,完成绝大多数任务都不需要借助其它技术,技术栈更统一,运维成本更低。
文件存储
在数据存储方面,SPL 与传统数据仓库也有很大不同。
传统数据仓库的数据组织在逻辑上是整体化的,某个主题下的数据形成一个数据库,有一套元数据来描述库内数据的结构及关系。数据有着明确的库内和库外的差别,这就是所谓的封闭性。不仅数据要存储在内部,而且使用时经常会绕过操作系统而直接操作硬盘,这会导致存储和计算深度绑定,不利于存算分离。这样的机制不利于云化,要使用网络文件系统以及云上的对象存储,需要从底层重构,会带来不少实施风险。
SPL没有元数据,直接采用文件存储,可以使用任意开放文件类型,SPL 为了保证计算性能还设计了专门的二进制文件格式。存储和计算不再绑定可以很方便实现存算分离,进而实施弹性计算,因此更易于云化。
文件存储的成本更低,在 AP 类计算场景下用户可以随意设计空间换时间的方案,无非就是多存几个文件,即使冗余数据文件多到上万(现代文件系统处理这个规模的文件数据很轻松)也完全没有负担。而且使用文件系统的树状结构很容易分门别类管理这些数据文件,运维成本更低。
延伸阅读: 跑在文件系统上的数据仓库
开放性
我们知道传统数据仓库是从数据库基础上发展来的,因此继承了数据库的诸多特性,包括元数据和数据约束,这就要求数据只有入库才能计算,相当于有个“城墙”,数据在墙内和墙外很严格,整体表现就是前面提到的封闭。
不过,现代数据应用的数据源更为广泛,经常会面对五花八门的数据来源和类型。封闭的数据库不能针对库外的数据开放其计算能力,就只能把外部数据先导入后才能计算。这会增加一步 ETL 动作,增大工作量并加大数据库负担的同时,还丧失了数据的实时性。这些外部数据的格式常常不规范,导入进有强约束的数据库并不是一件很容易的事。而且,即使做 ETL 也要先把未整理的数据先入库才能利用数据库的计算能力,结果把 ETL 做成 ELT,加大数据库负担。
SPL 具备更强的开放性。随便什么数据,只要能访问到就能一起参与运算,无非只是不同数据源的访问性能不同。SPL 为了高性能设计的文件存储其逻辑地位和某个文本文件或者 Restful 上取出来的数据并没有区别,来自 SQL 数据库的数据也一样可以处理。这就是所谓计算能力的开放性。开放的计算能力不再有“城墙”的限制使用效率更高,用于构建诸如湖仓一体也更加方便。
延伸阅读: 现在的湖仓一体像是个伪命题
更进一步,SPL 的开放性还表现在利用实时数据上。业务数据入仓伴随的 ETL 过程会导致数据时效性变差。而 SPL 支持异构源混合计算,方便完成 T+0 查询,冷数据基于数据仓库,热数据基于生产库(这部分数据很小查询不会造成太大负担),二者混合计算就能获得 T+0 查询结果。有了这个能力以后,就可以在不改变已有生产库的情况下构建 HTAP。
延伸阅读: HTAP 数据库搞不定 HTAP 需求
总结
SPL 作为程序语言、中间件和数据仓库表现得一体多面,这是源自结构化数据应用场景的多样性和 SPL 技术的开放性、敏捷性和高性能。传统数据处理技术在理论上几乎不再有突破,大家比拼的是工程上的优化能力,但有些理论上的不足很难通过工程手段来弥补,这就需要理论和工程上的双向加持,“新兴”的 SPL 提供了这种能力,十分值得一试。
英文版