在谈集合之前,需要先谈谈离散性的概念:

所谓离散性,是指集合的成员可以游离在集合之外存在并参与运算,游离成员还可以再组成新的集合。从离散性的解释上可以知道,离散性是针对集合而言的一种能力,离开集合概念单独谈离散性就没有意义了。

离散性是个很简单的特性,几乎所有支持结构(对象)的高级语言都天然支持,比如我们用Java时都可以把数组成员取出来单独计算,也可以再次组成新的数组进行集合运算(不过Java几乎没有提供集合运算类库)。

打个通俗的比方:假设有一个盒子里装满了白色小球,针对离散性的操作就相当于把盒子打开,把里面的小球一个个单独拿出来刷上不同颜色,则操作后每个小球的颜色都各不相同;而针对整个集合的操作,就相当于把装入一定数量小球的盒子,运到某个地方,则盒内所有小球也都同时被运到了那个地方。

回到程序的编写方向上,同时具备良好的集合运算类库与离散性引用机制的集算器脚本语言,相较于传统的SQL语言(受限于关系代数),无论从思考方式还是从执行效率上来看,都有着先天的优势。

一、解决一些逻辑稍微复杂一点的实际问题。

比如以前提到的:计算至少连涨四天的股票,在至少连涨三天的股票中所占的比例:

浅谈集合与引用_集合

一个比较普通的思路是用窗口函数:将数据按公司名分区后再按日期排序(Order By),调用LAG窗口函数向上做求差运算并根据是否为负记是否为NULL,调用LAG和LEAD窗口函数找出上升趋势和下降趋势的分段点并记1,再调用SUM窗口函数将分段点预设值累加从而成为分段的依据字段,然后清空之前用NULL标记的无效行后,再分别统计算出>=3和>=4的数目,最后算出一个比值。

具体实现代码如下(下面以SqlServer数据库为例):

WITH T1 AS

(

SELECT T.COM COM, T.STA STA, SUM(T.FLG) OVER(PARTITION BY T.COM ORDER BY T.DAT) GRP

FROM (

SELECT [Company] COM, [Date] DAT, [Price] PRI,

CASE WHEN [Price] > LAG([Price],1,0) OVER(PARTITION BY [Company] ORDER BY [Date])

THEN 1 ELSE NULL END STA,

CASE WHEN [Price] < LAG([Price],1,0) OVER(PARTITION BY [Company] ORDER BY [Date])

AND [Price] < LEAD([Price],1,9999999) OVER(PARTITION BY [Company] ORDER BY [Date])

THEN 1 ELSE 0 END FLG

FROM Stock

) T

),

T2 AS

(

SELECT T1.COM COM, T1.GRP GRP, COUNT(T1.COM) CNT FROM T1 WHERE T1.STA IS NOT NULL GROUP BY T1.COM, T1.GRP

),

T3 AS

(

SELECT COUNT(T2.COM) Up3Days FROM T2 WHERE T2.CNT >= 3

),

T4 AS

(

SELECT COUNT(T2.COM) Up4Days FROM T2 WHERE T2.CNT >= 4

)

SELECT CONVERT(FLOAT,T4.Up4Days,120)/CONVERT(FLOAT,T3.Up3Days,120) FROM T3 JOIN T4 ON 1=1

 

可以看出:这种方法在数据处理的过程中,对数据增加分类的定义与处理,实在太麻烦:除了几层的嵌套子查询,还得增加过滤和分段的标记、还得思考如何用分段标记形成分段字段,还得思考如何不重复查询同一个表浪费时间……那么有没有更灵活的方法呢?也许有,比如对于SqlServer还可以考虑使用游标等方法(虽然灵活不过代码量只怕更多……感觉T-SQL正无限接近Java中)

CREATE TABLE #RT(Company VARCHAR(20) PRIMARY KEY NOT NULL, Price DECIMAL NOT NULL, Record INT NULL, Most INT NULL)

CREATE TABLE #TT(Company VARCHAR(20) NOT NULL, Price DECIMAL NOT NULL, DT DATE NOT NULL)

CREATE CLUSTERED INDEX IDX_#TT ON #TT(Company,DT) –SQLSVR2016需要创建索引否则排序无效

INSERT INTO #TT SELECT [Company], [Price], [Date] FROM Stock ORDER BY [Company],[Date]

DECLARE @Company VARCHAR(20), @Price DECIMAL, @Record INT, @Most INT

SET @Price=0 –Price字段需要有初始值0

DECLARE iCursor CURSOR FOR SELECT Company, Price FROM #TT –定义游标

OPEN iCursor –开启游标

FETCH NEXT FROM iCursor INTO @Company, @Price –取第一行数据存入变量

WHILE @@FETCH_STATUS=0 –游标取数成功则进入循环

BEGIN

IF((SELECT COUNT(*) FROM #RT WHERE Company=@Company)=0)

BEGIN INSERT INTO #RT VALUES(@Company, @Price, 1, 1) END

ELSE

BEGIN

IF((SELECT TOP 1 Price FROM #RT WHERE Company=@Company)<@Price)

BEGIN

SET @Record = 1+(SELECT TOP 1 Record FROM #RT WHERE Company=@Company)

SET @Most = (SELECT TOP 1 Most FROM #RT WHERE Company=@Company)

UPDATE #RT SET Price=@Price, Record=@Record WHERE Company=@Company

IF(@Record>=3 AND @Most<@Record)

BEGIN UPDATE #RT SET Most=@Record WHERE Company=@Company END

END

ELSE

BEGIN UPDATE #RT SET Price=@Price, Record=1 WHERE Company=@Company END

END

FETCH NEXT FROM iCursor INTO @Company, @Price –继续取下一条数据否则会死循环

END

CLOSE iCursor –关闭游标

DEALLOCATE iCursor –释放游标内存

; –注意此处要用分号结尾否则WITH子句会报错

WITH T1 AS (SELECT COUNT(*) Num FROM #RT WHERE #RT.Most>=3),

T2 AS (SELECT COUNT(*) Num FROM #RT WHERE #RT.Most>=4)

SELECT CONVERT(FLOAT,T2.Num,120)/CONVERT(FLOAT,T1.Num,120) FROM T1 JOIN T2 ON 1=1 –计算最终结果

DROP TABLE #RT

DROP TABLE #TT

而且这样的写法基本上并不具有通用性,也就是说如果换个数据库,那你可能还需要再研究一次别的数据库中使用游标的方法。

 

再来看看集算器要搞定类似问题时需要的代码(为了方便起见数据源使用的Excel):


A
1=file(“E:/Stock.xlsx”).xlsimport@t().sort(Date).group(Company)
2=A1.((a=0,~.max(a=if(Price>Price[-1],a+1,0))))
3=string(A2.count(~>=4)/A2.count(~>=3),”0.00%”)

 

实现一个同样的目标,相比之下,集算器的代码不仅简洁、高效,而且适应性广,另外即使需要针对大数据量做特殊的并行计算处理时,也不会束手无策。

二、集算器在处理数据库数据上的便捷性

既然数据库SQL语言编程受到的限制这么多,写起来这么麻烦,那么存在数据库中的数据,难道就没法整治了吗?

当然不是,毕竟我们还有集算器这一法宝。下面再来看一个简单的计算:

如何对一个字段循环求和,当满足一个值(80)就退出循环,并能得到最后一次循环时各字段对应的值

浅谈集合与引用_集合_02

SqlServer的脚本程序如下:

with cte as (

select *,cnt3 sumcnt from Tb where cnt1=1

union all

select Tb.*, sumcnt+Tb.cnt3 from Tb join cte on 1+cte.cnt1=Tb.cnt1 where sumcnt+Tb.cnt3<=80

) select * from Tb where cnt1 = (select max(cnt1) from cte)

用上了with as子句的递归功能,这样确实可以在数据行数过多时,提前结束不必要的计算,节省了计算时间。但With As子句的递归在更复杂的应用中,还是比较难写的,毕竟稍不注意就可能陷入无限死循环;而且说实话,有些数据库可能也不支持with as子句的递归功能

 

还有另一种:

select top 1 cnt1, cnt3 from

(

select cnt1 cnt1, cnt3 cnt3, (select SUM(cnt3) from Tb b where b.cnt1<=a.cnt1) cnt_sum from Tb a

) c where cnt_sum<=80 order by cnt_sum desc

 

这个表面上看起来只用了两次子查询,但最里面的子查询执行逻辑并不是很好理解,其实它利用了SqlServer数据库底层对select执行流程的细节:先select出a表的cnt1和cnt3,然后在最里面那个子查询中根据a表的cnt1对b表做where子句过滤后,再计算b表cnt3的sum聚合值。而这就要求数据库执行语句顺序,必须确实是按照设计者的思维去执行,否则便可能出错或无法识别。因此这个方法也未必能够适用于所有数据库。

当然,以上两种SQL脚本运行结果在SqlServer上还是一样的:

浅谈集合与引用_引用_03

然后,我们再来看看集算器的办法:


A
1=connect(“SQLSVR”).query(“select * from Tb”)
2=A1.iterate((x=~[-1],~~+cnt3),0,~~>80)

其中变量x就是要计算的结果

浅谈集合与引用_集合_04

解释一下:A1中的代码是从SqlServer数据库中取数并建立序表对象,具体细节就不多说了,按照集算器自带教程,照猫画虎的操作就可以搞定。

真正发挥计算作用的是A2行的iterate函数,看起来感觉有点迷糊?恐怕那只是因为你比较习惯SQL而已。下面让我来告诉你这个函数用起来有多么简单。

iterate函数是一个循环函数,所谓循环函数就是会根据调用的他的序列或序表中所包含的元素个数决定最大循环次数。说的简单点,你可以把它想象成一个更加灵活的while循环:

iterate函数共有三个参数,这里可记为iterate(a,b,c),还包含一个用于保存每次循环计算得到结果的隐藏变量:~~,以及一个指向当前序列元素或序表记录的类似于指针的变量:~。

iterate(a,b,c)的调用顺序是b->a->c,其中b用于赋予计算结果变量~~一个初值,a则是每次循环都会计算参数表达式并赋值~~,c则是一个布尔表达式,当表达式的值为真时函数会提前结束循环。(注意:是为真时退出循环)

说白了iterate(a,b,c)就相当于下面用while循环模拟的伪代码的示意(注意~和~~是变量,a、b、c是表达式):

i = 0;

~~ = b;

while (i <= len && !c) {

~ = A(++i);

~~ = a;

}

怎么样,看完是不是觉得浑身一阵清爽:原来编程其实可以这么简单!

三、集算器支持的集合运算类库

既然离散性高,语法灵活,好处有这么多,那么是否就可以一味的追求离散性,而忽视集合运算的重要?当然也不是。

离散性高,虽然让编程语言(比如Java,更甚者如C++)语法灵活,解决复杂问题时,也比离散性差的语言(比如SQL,其次如Python)优势明显。但在解决常见的简单问题,尤其是某一限定领域的问题时,更专业化的语言往往比适应面更广的语言,更能让编程人员快速高效地开发出有效的代码来。这在当前社会追求各种工程的效率的环境下,更显得尤为重要。

比如最简单的例子:读一张Excel表,计算一下按分组字段(STYLE,BEDROOM)分组后,另一数值字段(Price)的平均值

浅谈集合与引用_引用_05

用集算器算的话,非常简便


A
1=file(“D:/data.xlsx”).xlsimport@tc()
2=A1.groups(STYLE,BEDROOMS;avg(SQFEET):SQFEET,avg(BATHS):BATHS,avg(PRICE):PRICE)

 

当然与集算器有点类似的python,也有这类的运算库,这恐怕也是python最近异常火爆的原因之一

import pandas as pd

data = pd.read_excel(‘D:/data.xlsx’,sheet_name=0)

print(data.groupby([‘STYLE’,BEDROOMS’]).mean())

 

但是……你能想象用没有提供类似集合运算的类库的Java甚至C++,来实现同样的功能吗?只怕光是读Excel都够做个模块了,然后是分组与聚合的计算,还有报表对象的构建,甚至运算结果的显示功能(单单想象一下都感觉很累)正因为如此,集算器的语法在设计之初,就考虑到了集合性运算与离散性引用,这对看似矛盾却缺一不可的客观需求。如果说武功的最高境界乃是阴阳互济的话,那么编程语言的最高境界,我觉得恐怕就是集合与离散的优点兼而有之吧。