告别窗口函数,迎来 esProc SPL

早期 SQL 对有序计算极端不适应,理论上可以写,但实际的麻烦程度基本上等同于不能用。

在引入窗口函数后,有序计算得到了一定的改善,但 SQL 的基础还是无序集合,即使打了窗口函数这样的补丁也还是很麻烦。

但也没有什么更好的办法,基于数据库的运算还是要用 SQL 加窗口函数。把数据搬出来用 Python 可以缓解这些麻烦,但 Python 集成性不好,和 Java 应用不容易配合。直接用 Java 写的话,会比用窗口函数还麻烦。

用 esProc SPL 就可以从根本上解决这些问题。SPL 代码比 SQL 窗口函数更易理解,编写和调试都很容易,我们可以和麻烦的窗口函数说再见了。

这里通过具体的计算场景来看 SPL 如何完胜窗口函数。

比如要计算某股票上市后用了多少个交易日涨到 100 元以上。简单的思路是:把数据按照交易日排序后,找到第一个 100 元以上的记录位置,对应的序号就是计算目标。

用 SQL 窗口函数写出来:

SELECT MIN(NO) 
FROM (
	SELECT Price, ROW_NUMBER() OVER ( ORDER BY Date ) NO
        FROM stock
      )
WHERE Price>100

先用窗口函数计算出序号,再用外层查询做过滤并求最小值。

SQL 基于无序集合,数据表本身没有顺序和位置概念,对数据表排序也无法被窗口函数利用,只能把 ORDER BY 写到窗口函数中。这是 SQL 固有的问题,窗口函数也只能继承 SQL 这个缺点。

麻烦还不止于此,SQL 语句中的 NO 是一个计算列,却不能直接求最小值,要再嵌套一层查询才行。

不包含窗口函数的计算列是可以直接求最小值的,比如收盘价乘以当天汇率后求最小值:

SELECT MIN(Price*Rate) FROM stock

窗口函数只能基于其它运算的结果集再运算,经常要再嵌套一层查询,语法比普通 SQL 麻烦。

SPL 数据表有序,且有位置概念。对于日期有序的股票数据,可以按照前面提到的简单思路计算,写出来的代码极致简洁:

stock.pselect(Price > 100)

pselect 是 SPL 的位置计算函数,可以得到满足条件的第一个成员的位置,也就是该股票第一次涨到 100 元以上所用的交易日个数。

现在要进一步计算该股票第一次涨到超过 100 元以上时的涨幅。我们可以先找到第一条收盘价大于 100 元的记录位置,用这条记录收盘价和它前一条的相减就可以了,仍是很自然的思路。

用 SQL 窗口函数就更绕了:

WITH T AS (
    SELECT Price,
        ROW_NUMBER() OVER ( ORDER BY Date ) NO,
        Price - LAG(Price) OVER ( ORDER BY Date) Rising
    FROM Stock
    )
SELECT Rising 
FROM T WHERE NO = (
    SELECT MIN(NO) FROM T WHERE Price>100 
   )

SQL 不能实现上面的先找到位置再算相邻差值的思路,要改成先计算所有行相关的差值再寻找位置的方法。而且由于 ORDER BY 只能写到窗口函数中,结果就要重复写两次。如果还有 PARTITION 之类也要重复写,这就造成代码中有很多重复信息,编写和阅读代码都会受到影响。

SPL 支持位置计算,就非常简单:

stock.calc(stock.pselect(Price>100),Price-Price[-1])

先用 pselect 计算出需要的记录位置,再计算这个位置上的收盘价和上一条记录收盘价的差值。SPL 支持跨行计算,Price[-1] 就是上一条记录的收盘价。

这种情况在现实中很常见,我们继续用股票计算来看更多例子。

求股票最长连涨区间的起止日期。

窗口函数的解法:

with t1 as (
    select Date, Price, 
	    case when Price>lag(Price) over(order by Date) then 0 else 1 end UpDownTag
    from stock
    ),
t2 as (
    select Date, Price, sum(UpDownTag) over(order by Date) NoRisingDays
    from t1
    ),
t3 as (
    select NoRisingDays, count(*) ContinuousDays 
    from t2 group by NoRisingDays
    ),
t4 as (
    select * 
    from t2
    where NoRisingDays in (
	    select NoRisingDays
	    from t3
	    where ContinuousDays =(select max(ContinuousDays) from t3)
            )
    )
select min(Date) start, max(Date) end
from t4
group by NoRisingDays;

嵌套多层子查询,窗口函数中相同的 order by 重复出现,很麻烦。

SPL 避免了难懂的窗口函数,代码非常简捷:

stock.group@i(Price<Price[-1]).maxp@a(~.len()).new(~(1).Date:start,~.m(-1).Date:end)

group@i 是按照条件变化有序分组,每组都是连涨区间。maxp@a 返回所有最长的连涨区间。~ 符号代表循环计算的当前成员。

找出所有股票中,最长连涨天数超过 5 天的股票。

SQL 解法:

select Code, max(ContinuousDays) as longestUpDays
from (
    select Code, count(*) as ContinuousDays
    from (
        select Code, Date, 
            sum(UpDownTag) over (partition by Code order by Code, Date) as NoRisingDays
        from (
            select Code, Date, 
                case when Price > lag(Price) over (partition by Code order by Code, Date)  then 0
                else 1 end as UpDownTag
            from stock
        )
    )
    group by Code, NoRisingDays
)
group by Code
having max(ContinuousDays)>5;

嵌套四层子查询,相同的 partition 和 order 信息重复出现。

SPL 仍然很简单:

stock.group(Code;~.group@i(Price<=Price[-1]).max(~.len()):max_increase_days).select(max_increase_days>5)

计算股价高于前后 5 天时当天的涨幅。

窗口函数这样写:

SELECT Price/Price_pre-1 AS raise
FROM(
    SELECT Date, Price, ROWNUM AS rn,
        MAX(Price) OVER (
            ORDER BY Date ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING) AS max_pre,
        MAX(Price) OVER (
            ORDER BY Date ROWS BETWEEN 1 FOLLOWING AND 5 FOLLOWING) AS max_suf,
        LAG(Price,1) OVER (ORDER BY Date) AS Price_pre
    FROM stock
    )
WHERE rn>5 AND rn<=(select count(*) FROM stock)-5 AND Price>max_pre  
AND Price>max_suf;

窗口函数中相同的 order by 信息重复了三次,还要嵌套子查询。

SPL 仍然很简捷:


A

1

=stock.pselect@a(#>4&&#<=stock.len()-4&&Price>max(Price[-4:-1])&&Price>max(Price[1:4]))

2

=stock.calc(A2,Price/Price[-1]-1)

#表示循环计算中当前的序号。Price[a:b] 表示股票有序集合中,从 #+a 到#+b 的收盘价集合。

其它业务场景中这样的情况也很普遍,不能尽述。

利用 SPL 的计算能力能避免窗口函数,这样可以让 SQL 只负责取数,复杂逻辑都交给 SPL 处理。比如上个例子,假设数据来自于 Mysql 数据库:


A

1

=connect("mysql").query("select * from stock order by Date")

2

=A1.pselect@a(#>4&&#<=A1.len()-4&&Price>max(Price[-4:-1])&&Price>max(Price[1:4]))

3

=A1.calc(A2,Price/Price[-1]-1)

A1 连接数据库,用 SQL 语句取数。

A2 开始用非常简捷的 SPL 代码完成复杂计算。

采用 SPL 后,应用中只要写简单 SQL,不必再使用复杂难懂的窗口函数了。而且,不同数据库窗口函数的写法是有差别的,这样做,还能解决异构数据库的兼容性问题,一举多得。