淺談集合與引用
所謂離散性,是指集合的成員可以遊離在集合之外存在並參與運算,遊離成員還可以再組成新的集合。從離散性的解釋上可以知道,離散性是針對集合而言的一種能力,離開集合概念單獨談離散性就沒有意義了。
離散性是個很簡單的特性,幾乎所有支持結構(對象)的高級語言都天然支持,比如我們用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 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上還是一樣的:
然後,我們再來看看集算器的辦法:
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都夠做個模塊了,然後是分組與聚合的計算,還有報表對象的構建,甚至運算結果的顯示功能(單單想象一下都感覺很累)正因為如此,集算器的語法在設計之初,就考慮到了集合性運算與離散性引用,這對看似矛盾卻缺一不可的客觀需求。如果說武功的最高境界乃是陰陽互濟的話,那麽編程語言的最高境界,我覺得恐怕就是集合與離散的優點兼而有之吧。
淺談集合與引用