浅谈集合与引用

【摘要】

        集合与引用,便好似一对天生的冤家对头。特别注重集合运算的语言,引用变量的方法往往非常受限(如 SQL);而拥有灵活的引用方式的语言,又往往缺少便捷的集合运算的方法(如 C 语言)。
        是否能在这看似矛盾的二者之间,找到一个完美的平衡点?传说中的 SPL 语言,又是怎样达到一个刚柔并重、阴阳互济的完美境界的?我们不妨去乾学院看个究竟:浅谈集合与引用!

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

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

离散性是个很简单的特性,几乎所有支持结构(对象)的高级语言都天然支持,比如我们用 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)就退出循环,并能得到最后一次循环时各字段对应的值

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 top1 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_sumd esc

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

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

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

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

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

解释一下: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)的平均值

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

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 都够做个模块了,然后是分组与聚合的计算,还有报表对象的构建,甚至运算结果的显示功能(单单想象一下都感觉很累)正因为如此,集算器的语法在设计之初,就考虑到了集合性运算与离散性引用,这对看似矛盾却缺一不可的客观需求。如果说武功的最高境界乃是阴阳互济的话,那么编程语言的最高境界,我觉得恐怕就是集合与离散的优点兼而有之吧。