SPL的理论(二)

SPL的理论

SPL(Structured Process Language)是由润乾公司研发的一种数据处理语言,旨在应对复杂数据处理场景,特别是结构化数据的计算需求。开发了不同于传统关系代数的计算体系,提出了离散数据集模型,并在此基础上构建了SPL及其相关计算产品——集算器。在2021年底,SPL产品进行了部分开源。

基本概念

集合

一些确定的数据构成集合,构成集合的数据称为集合的元素。在{}中写上元素表示

集合,如{1,2,3,…}是所有自然数的集合。{x|P(x)}表示一切具有性质 P 的数据构成的集合。

有序集合

有限集合的元素次序被关注时被称为有序集合,为与无序集合区分,在[]而不是{}中依次写上元素以表示有序集,如 [a1,…,an]。

排列

数据表是记录构成的集合,而构成某个数据表的记录还可以用于构成其它数据表(排列)。比如过滤运算就是用原数据表中满足条件的记录构成新数据表,是一种引用关系。

但是在SQL中,关系代数没有可运算的数据类型来表示记录,单记录实际上是只有一行的数据表,不同数据表中的记录也不能共享。比如,过滤运算时会复制出新记录来构成新数据表,空间和时间成本都变大。

分组

分组的实质动作是将一个集合拆成一些小集合,所以返回值是一个序列的序列。

分组的拆分有多种拆分方式,可能是根据集合中的某一列的计算值或外部序列的值来拆分这个集合。

离散数据集模型

Java 和SQL中数据模型的不同

Java 等高级程序语言中的数据都是以一些不可以再拆分的原子数据为基础的,比如数、串等。原子数据可以构成集合和记录等较复杂的数据,集合和记录也是某种数据,也可以再组合成更大的集合和记录。而构成集合和记录的数据并不依附于集合和记录,可以独立存在和参与计算,自然会提供从集合和记录中拆解出成员的操作(用序号和字段取成员)。这种自由的数据组织形式,我们称为离散性。支持了离散性后,可以轻易地构造出集合的集合、以及字段取值是记录的记录。

SQL 的数学基础是关系代数,是用来实现批量结构化数据计算的代数体系,这也是采用 SQL 的数据库又被叫做关系数据库的原因。

关系代数延用了数学上的无序集合理论,没有给 SQL 造出序的概念。SQL把表(也就是记录的集合)作为一种原子数据,它并不是由更基础的原子数据组合成的。SQL 的数据也没有可组合性,集合的集合和记录的记录在 SQL 中是不存在的。SQL 中记录必须依附于表,不能独立存在,也就是成员必须依附于集合,拆解集合成员的操作是没有意义的,因为拆出来也没有对应的数据类型来承载。所谓拆解,其实是用 WHERE 这种过滤运算,这就会显得有点绕。过滤运算本质上是在计算子集,结果仍然是表(集合),单条记录实质上是只有一条记录的表(只有一个成员的集合)。SQL 这种数据组织方式很不自由,缺失了离散性。

几乎所有高级语言都天然支持离散性,然而 SQL 没有。

SQL的集合化不彻底,没有游离记录及其构成的集合

SQL 难度大的问题几乎都是缺失离散性造成的。没有集合的集合,SQL 在分组时无法保持分组子集,必须强迫聚合,SQL 的集合化不彻底。没有游离记录及其构成的集合,只能用外键表示数据之间的关联关系,代码繁琐又难懂,运算性能还差,缺乏离散性的SQL 无法采用直观的引用机制描述关联。特别地,没有离散性的支持,SQL 很难描述有序计算,有序计算是典型的离散和集合的结合物,成员的次序在集合中才有意义,这要求集合,有序计算时又要将每个成员与相邻成员区分开,会强调离散。

SPL采用离散数据集的结构。

序列

一批数据按次序可以构成一个序列,起一个变量名来存储和命名,这些构成序列的数据称为序列的成员,构成序列的成员个数称为序列的长度

很多程序语言中,数组的成员必须是同一种数据类型。但在 SPL 中没有做这个要求,序列的成员可以有不同数据类型,不过大多数情况序列还是由相同数据类型成员构成。

用在序列变量后用圆括号加序号,就可以访问序列中这个序号对应的成员,取值和赋值都可以:

这个加了# 的变量通常称为循环序号

图形用户界面, 应用程序

描述已自动生成

如果是基本类型,那么就是复制数据

把序列这类复杂的数据类型称为对象

在传递的时候,是以引用方式进行传递的。如果修改,会修改原始值。

图形用户界面, 文本, 应用程序

描述已自动生成

序列的集合运算

A1|B1 将序列 B1 的成员追加到序列 A1 后面形成新序列返回,这个运算称为和列。&、^、\ 则分别是通常意义的集合并、交、差运算。我们在前面用过 \ 表示整除运算,这里在序列上表示差集。

注意通常意义的并:如果2个集合有重复的,那么重复的只会记录一次

但是和列|,如果2个集合有重复,它在和的时候,不考虑这个因素,它会直接添加在后面。

需要注意的是,和数学上的无序集合不同,序列是成员是有序的,这会导致 A1&B1 和 B1&A1 并不一定相同,SPL 中的序列交、并运算不满足交换律。有序集合还允许重复的成员,这时候再做交、并、差运算时,会比无重复成员的集合会更为复杂,请仔细观察上面的运算结果以了解这时候的运算规律。

迭代函数

作为循环函数,A.iterate@a(x,a) 将针对 A 的每个成员计算 x,在 x 中可以使用 ~ 和 #表示循环中的 A 的当前成员和序号,这和其它循环函数相同。不同的是,迭代函数中还提供了符号 ~~,用于表示上一轮循环中计算出的 x,还开始循环时,~~ 的初始值是参数 a。所有成员都循环后,返回每一轮计算出来的 ~~ 构成的序列,其长度和 A 相同。

没有 @a 的函数 A.iterate(x,a) 则定义为 A.iterate@a(x,a) 的最后一个成员,即不再把中间过程收集成序列,只保留最后的 ~~

形状, 矩形

描述已自动生成

A7=5050

【程序设计】5.4 [一把抓] 迭代函数 * - 乾学院

需要注意的是, pos pseg 不是循环函数,其参数中的 x 中不能也不必再使用 ~# 等符号,如果使用了,则被认为是上一层循环函数的(这些函数可能被嵌套在循环函数中)。

A.pselect(x) 函数可以帮助我们,它将返回使 x true 的成员序号。换用循环函数的符号来说,就是 select 会返回由 ~ 构成的序列,而 pselect 将返回对应的 #pselect 被称为定位函数

@a 这种写法是 SPL 发明的,称为函数选项。理论上讲, pselect 和 pselect@a 是两个无关的不同函数,但这两个函数非常像,都是针对序列的,参数也一样,完成的功能虽然不完全相同,但也很类似。这种时候,我们更愿意把其中一个看成是另一个的变种,在起名时使用相似的名字,这样对于理解和记忆都会更方便一点。

但这两个函数总还是有不同,需要区分。在 SPL 中我们用相同的名字来命名这两个函数,而用函数名后 @后面的字符来区分,好象一个函数有了不同的模式:没有 @a 时,pselect 只返回第 1 个,有 @a 时将返回所有。

所以 SPL 约定除法结果总是一个浮点数,以保证这个运算结果的数据类型的确定性,否则你可能就搞不清这一步会算出来什么。在 SPL 中,还提供了一定返回整数的除法,用反斜杠 \ 表示,你可以试试 =6\3 会计算出什么,再试试 =5\3

图片包含 日历

描述已自动生成

注意,这个 if 并不是上一节说的 if() 函数,它后面可以没有括号(有也可以,括号会被当成逻辑表达式的一部分,在表达式外套一层括号不会改变表达式的计算结果),而且一定要写到格中代码的开头。

这是 if 语句最简单的形式:条件成立则向右执行,否则直接向下。

SPL的应用特点

普遍集合

关系代数中有集合的概念,但只处理由记录。离散数据集的集合概念更为普遍,可以处理任何数据构成的集合,即序列。这些集合,无论是不是由记录构成,都能执行一些共同运算,如聚合、过滤等。离散数据集支持由集合构成的集合,实现真正的分组。在分组聚合运算的时候,返回的不仅仅可以是一个单值聚合值,也可以是一个分组子集合。

游离记录

离散数据集中的记录是一种基本数据类型,它可以不依赖于数据表而独立存在。数据表

是记录构成的集合,而构成某个数据表的记录还可以用于构成其它数据表(排列)。比如过

滤运算就是用原数据表中满足条件的记录构成新数据表,这样,无论空间占用还是运算性能都更有优势。

关系代数没有可运算的数据类型来表示记录,单记录实际上是只有一行的数据表,不同

数据表中的记录也不能共享。比如,过滤运算时会复制出新记录来构成新数据表,空间和时间成本都变大。特别地,因为有游离记录,离散数据集允许记录的字段取值是某个记录,这样可以更方便地实现外键连接。

这个与高级语言一样,如java中的对象相互引用。

有序性

关系代数是基于无序集合设计的,集合成员没有序号的概念,也没有提供定位计算以及相邻引用的机制。SQL 实践时在工程上做了一些局部完善,使得现代 SQL 能方便地进行一部分有序运算。离散数据集中的集合是有序的,集合成员都有序号的概念,可以用序号访问成员,并定义了定位运算以返回成员在集合中的序号。离散数据集提供了 [] 符号以在集合运算中实现相邻引用,并支持针对集合中某个序号位置进行计算。

关系代数是基于无序集合设计的,集合成员没有序号的概念,也没有提供定位计算以及

相邻引用的机制。SQL 实践时在工程上做了一些局部完善,使得现代 SQL 能方便地进行一部分有序运算。

离散数据集中的集合是有序的,集合成员都有序号的概念,可以用序号访问成员,并定

义了定位运算以返回成员在集合中的序号。离散数据集提供了[]符号以在集合运算中实现相邻引用,并支持针对集合中某个序号位置进行计算。

有序运算很常见,却一直是 SQL 的困难问题,即使在有了窗口函数后仍然很繁琐;SPL

则大大改善了这个局面。

例 1:计算股价最高三天的涨幅

SQL:即使有窗口函数,仍然要复杂动作才能实现定位计算

SELECT date, price, seq, rate

FROM (

SELECT date, price, seq,

price/LAG(price,1) OVER (ORDER BY date ASC) rate

FROM (

SELECT date, price,

ROW_NUMBER() OVER (ORDER BY price DESC) seq

FROM stock ) )

WHERE seq<=3

SPL:有序集合支持下的定位计算很简单

T=stock.sort(date)

P=T.ptop(3,price)

T.calc(P,price/price[-1]-1)

例 2:计算一支股票最长连续上涨了多少天?

SQL:没有方便的相邻引用机制,要转换成复杂的分组问题

SELECT MAX(ContinuousDays)

FROM (

SELECT COUNT(*) ContinuousDays

FROM (

SELECT SUM(RisingFlag) OVER (ORDER BY date) NoRisingDays

FROM (

SELECT date,

CASE WHEN price>LAG(price) OVER (ORDER BY date) THEN 0

ELSE 1 END RisingFlag

FROM stock ) )

GROUP BY NoRisingDays )

SPL:有了相邻引用和迭代函数,很容易按自然思维实现

n=0

stock.sort(date).max(if(price>price[-1],n+1,0))

分组与聚合的分离

分组运算的本意是将一个大集合按某种规则拆成若干个子集合,关系代数中没有数据类型能够表示集合的集合,于是强迫在分组后做聚合运算。

离散数据集中允许集合的集合,可以表示合理的分组运算结果,分组和分组后的聚合被拆分成相互独立的两步运算,这样可以针对分组子集再进行更复杂的运算。

关系代数中只有一种等值分组,即按分组键值划分集合,等值分组是个完全划分。

离散数据集认为任何拆分大集合的方法都是分组运算,除了常规的等值分组外,还提供了与有序性结合的有序分组,以及可能得到不完全划分结果的对位分组和枚举分组。

关系代数中定义了丰富的集合运算,即能将集合作为整体参加运算,比如聚合、分组

等。这是 SQL 比 Java 等高级语言更为方便的地方。

但关系代数的离散性非常差,没有游离记录。而 Java 等高级语言在这方面则没有问

题。

离散数据集则相当于将离散性和集合化结合起来了,既有集合数据类型及相关的运算,也有集合成员游离在集合之外单独运算或再组成其它集合。可以说 SPL 集中了 SQL 和Java 两者的优势。

有序运算是典型的离散性与集合化的结合场景。次序的概念只有在集合中才有意义,单个成员无所谓次序,这里体现了集合化;而有序计算又需要针对某个成员及其相邻成员进行

计算,需要离散性。

在离散性的支持下才能获得更彻底的集合化,才能解决诸如有序计算类型的问题。

离散数据集是即有离散性又有集合性的代数体系,关系代数只有集合性。

5. 分组理解

分组运算的本意是将一个大集合按某种规则拆成若干个子集合,关系代数中没有数据

类型能够表示集合的集合,于是强迫在分组后做聚合运算。

离散数据集中允许集合的集合,可以表示合理的分组运算结果,分组和分组后的聚合

被拆分成相互独立的两步运算,这样可以针对分组子集再进行更复杂的运算。

关系代数中只有一种等值分组,即按分组键值划分集合,等值分组是个完全划分。

离散数据集认为任何拆分大集合的方法都是分组运算,除了常规的等值分组外,还提供

了与有序性结合的有序分组,以及可能得到不完全划分结果的对位分组。

基于有序分组运算,上述例 2 的 SQL 实现思路用 SPL 写出来也更为简单:

stock.sort(date).group@i(price>price[-1]).max(~.len())

6. 聚合理解

关系代数中没有显式的集合数据类型,聚合计算的结果都是单值,分组后的聚合运算

也是这样,只有 SUM、COUNT、MAX、MIN 等几种。特别地,关系代数无法把 TOPN 运算看成是聚合,针对全集的 TOPN 只能在输出结果集时排序后取前 N 条,而针对分组子集则很难做到 TOPN,需要转变思路拼出序号才能完成。

离散数据集提倡普遍集合,聚合运算的结果不一定是单值,仍然可能是个集合。在离散数据集中,TOPN 运算和 SUM、COUNT 这些是地位等同的,即可以针对全集也可以针对分组子集。

例 3:选出每天价格最高的三支股票

SQL:先要计算出序号再选出

SELECT *

FROM (

SELECT *,

ROW_NUMBER() OVER (ORDER BY price DESC PARTITION BY date) seq

FROM stock )

WHERE seq<=3

SPL:把 TOPN 当作聚合运算直接分组汇总

stock.groups(date;top(3;-price):top3).conj(top3)

SPL 把 TOPN 理解成聚合运算后,在工程实现时还可以避免全量数据的排序,从而获得高性能。而 SQL 的 TOPN 总是伴随 ORDER BY 动作,理论上需要大排序才能实现,需要寄希

望于数据库在工程实现时做优化。

表连接

表连接分为1对1,1对多,多对1

SQL 对 JOIN 的定义非常简单,就是对两个集合(表)做笛卡尔积后再按某种条件过滤,默认 JOIN 都是等值 JOIN,即过滤条件是一个或多个相等关系(多个之间是 AND 关系),语法形如 A JOIN B ON A.ai=B.bi AND …,其中 ai 和 bi 分别是 A 和 B 的字段。

等值 JOIN 还可以衍生出 LEFT JOIN 和 FULL JOIN,而且一般会被分成一对一、一对多、多对多等几种情况。

SQL 通常使用 HASH 算法来做内存连接,需要计算 HASH 值和比对。SPL采用外键地址化,性能优势非常明显。

在spl中,表连接分为外键表,同维表,主子表。1对多与多对1在SPL中,处理的方式不一样,后续章节有详细描述。

7. 连接理解

关系代数中,连接运算被定义为笛卡尔积再过滤,其中没有约定过滤条件的要求,这

样,理论上只要运算结果是笛卡尔积的子集,都可以认为是连接运算。

离散数据集将连接运算分成了三种情况:

1. 外键连接

表 A 的某些字段与表 B 的主键关联,B 表称为 A 的外键表,A 表称为事实表。

2. 同维连接

表 A 的主键与表 B 的主键,A 表和 B 表互称为同维表。

3. 主子连接

表 A 的主键与表 B 的主键的部分字段关联,A 表称为 B 的主表,B 表称为 A 的子表。

同维连接和主子连接可以再合并称为主键连接。

可以看出,离散数据集中定义的连接运算有这样的特点:

1. 只有等值连接(连接条件是关联字段相等)

2. 都会和某个表的主键相关

在实际应用过程中,绝大多数连接运算都是等值连接,而且有业务意义的连接也几乎

都和主键相关,和主键(指逻辑上的)无关的连接运算,通常会是个错误,因为会导致多

对多的关联效果。

离散数据集也设计了笛卡尔积后再过滤的运算,以处理极少数用上述定义不能覆盖的

场景,但并不把这种运算看成是常规的连接。

离散数据集对连接运算进行改造后,会获得一些好处。

把外键连接和主键连接区分开,可以更清晰看出数据表之间的关系结构。外键表通常

是用于进一步说明某些字段的详细信息,在理解事实表时可以先忽略它。外键连接和主子

连接虽然都可能有一对多的情况,但对于理解业务时的地位并不等同。

这样定义的连接剔除了多对多的情况,在处理较多表关联时,不会因漏写某个关联条

件而产生多对多的错误逻辑(常常可能把数据库跑死)。

在游离记录的支持下,可以把事实表中的字段直接赋值为外键表的相应记录,这样书

写起来要更直观且不易出错。

例 4:取出部门经理是中国人的美国人员工

SQL:关联关系不直观,同表两次连接要起别名

SELECT A.* FROM employee A

JOIN department B ON A.department=B.id

JOIN employee C ON B.manager=C.id

WHERE A.nation='US' AND C.nation='China'

SPL:在建立外键关系之后,关联条件很直观

employee.switch(department,department)

department.switch(manager,employee)

//上面动作可以在加载数据时做好,实际查询只有下面一句

employee.select(nation=="US" && department.manager.nation="China")

离散数据集的连接定义,还有在性能优化上的巨大优势,我们会在后面再谈到。

关系代数对连接的定义很简单,好处是可以涉及到非常广泛的范围,但同时也会缺失

一些关键特征,而无法利用这些特征简化代码书写和实现性能优化。

外存计算

关系代数是个纯数学层面的理论体系,没有考虑内存和外存的区别。

离散数据集则考虑了工程实现的方案,为外存计算做了专门的设计。

离散数据集抽象了游标对象,用于处理只能顺序访问不能随机访问的数据,这符合不特

定存储设备上的外部数据的特征。提出并利用导出游标的框架,将大多数游标运算的与对应的序表运算设计得非常相似,尽量提高外存计算的透明性。基于游标运算编写的计算逻辑,可以很轻松地应用到这些外部存储的数据上。

离散数据集中还设计了提供有限随机访问能力的组表用于外部存储,这是根据硬盘这

种最常见存储设备的特征设计的。同样地,基于组表运算编写的计算逻辑,也很容易在硬

盘上高速实现。

附表机制允许一个组表同时存储多个关联的数据表。主键连接的关系通常在数据结构

设计时就已经确定,不会在计算过程中临时指定表与关联字段。将主键连接的若干表事先

存储在一起,相当于预先关联,可以减少存储量及关联计算量,获得更优性能。这也是利

用了离散数据集对连接运算的理解,外键连接情况就不可以事先关联。

9. 有序下的高性能

离散数据集特别强调有序集合,利用有序的特征可以实施很多高性能算法。这是基于

无序集合的关系代数无能为力的,只能寄希望于工程上的优化。

这里简要罗列一些利用有序特征后可以实施的低复杂度运算。

1)

组表对键有序,相当于天然有一个索引。对键字段的过滤经常可以快速定位,以减少

外存遍历量。随机按键值取数时也可以用二分法定位,在同时针对多个键值取数时还能重

复利用索引信息。

2)

通常的分组运算是用 HASH 算法实现的,如果我们确定地知道数据对分组键值有序,则

可以只做相邻对比,避免计算 HASH 值,也不会有 HASH 冲突的问题,而且非常容易并行。

3)

组表对键有序,两个大组表之间主健连接可以执行更高性能的归并算法,只要对数据

遍历一次,不必缓存,对内存占用很小;而传统的 HASH 分堆方法不仅比较复杂度高,需要较大内存并做外部缓存,还可能因 HASH 函数不当而造成二次 HASH 再缓存。

键有序的组表,还可以利用分位点拆分成多段,实现并行计算。HASH 算法要实现并行

时需要占用较大内存,难以将并行度提高。

若参与连接的某一个组表被过滤后变小,则可以利用键有序的特征,快速定位另一个

组表中对应的数据,避免全表遍历。

4)

大组表作为外键表的连接。事实表小时,可以利用外键表有序,快速从中取出关联键

值对应的数据实现连接,不需要做 HASH 分堆动作。事实表也很大时,可以将外键表用分位点分成多个逻辑段,再将事实表按逻辑段进行分堆,这样只需要对一个表做分堆,而且分堆过程中不会出现 HASH 分堆时的可能出现的二次分堆,计算复杂度能大幅下降。

其中 3 和 4 利用了离散数据集对连接运算的改造,如果仍然延用关系代数的定义(可

能产生多对多),则很难实现这种低复杂的算法。

结语

以上是离散数据集和关系代数的重点差异。在工程实现方面,离散数据集还有一些优

势,比如更易于并行、大内存预关联提高外键连接性能等,以及一些在应用时的注意事

项,比如怎样保证在数据在物理上有序、如何存储数据以支持随意分段并行等。限于篇

幅,我们舍去了这些理论色彩相对较弱的内容,对已写内容也没有做深入的展开解释,不

过已经可以从中窥探出这两种代数体系的不同。

我们已经完成了 SPL 的工程实现,在乾学院上还有更多 SPL 的相关资料,包括技术原

理以及实践案例,以及与关系数据库上 SQL 在敏捷计算和性能提升方面的差异对比,感兴

趣的读者可以前去参考。

本文仅涉及 OLAP 业务,也就是只关注数据计算,对于 OLTP 业务没有提及。可以说当

前的离散数据集更适合用于数据仓库的理论基础。事实上,关系代数作为 OLTP 业务的理论

基础也有诸多问题,比如无法支持数据结构的多样性、实现事务一致性的成本太高等。我

们正在研究,在未来将发布进一步的想法,以解决关系代数处理 OLTP 业务时的困难。

关系代数发布于 50 年前,当时的应用需求、计算机硬件环境和当前相比,都有了巨大

的变化。当时非常适应的模型体系现在不适应也是很正常的,从这个意义上讲,离散数据

集可以认为是关系代数的一个进化版,但并不是完全向上兼容版。

层次参数

结构化运算函数的参数有些很复杂。SPL 使用层次参数简化复杂参数的表达,即通过分号、逗号、冒号自高而低将参数分为三层:

比如,在各部门找出比本部门平均年龄小的员工:

=A2.group(DEPT; (a=~.avg(age(BIRTHDAY)),~.select(age(BIRTHDAY)<a)):YOUNG)        

分号的优先级最高,其次是逗号,最后是冒号

join(Orders:o,SellerId ; Employees:e,EId)

动态数据结构

可根据上一步的计算结果推断出新数据结构,并自动生成新序表,新序表可直接进行计算。比如先分组汇总,再过滤,最后排序:

T.groups(SellerId, Client; sum(Amount):amt, count(1):cnt).select(amt>10000 && amt<=30000 && like(Client,“*bro*”)).sort(amt)

使用支持动态数据结构的序表,开发者可以更加关注计算本身,而不是思考如何事先定义结果集。这样的编码风格不仅简短易懂,而且更符合自然思维,开发效率可以显著提升。在多步骤的复杂业务逻辑中,动态数据结构带来的优势更加明显。

在Java代码中,动态数据结构一般采用Map的方式,但是每一步无法直观的看到结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值