SQL 和 SPL 的有序運算對比
【摘要】
有序運算是指按照一定的次序對有序集合的成員進行計算。SQL 和 SPL 是大家比較熟悉的程式語言,本文將探討對於有序運算問題,這兩種語言的解決方案和基本原理。如何簡便快捷的處理有序運算,這裡為你全程解析,並提供 SQL 和 SPL 示例程式碼。
使用過 SQL 或 SPL 的朋友對計算欄位都不會陌生,比如 firstname+lastname,year(birthday),這些計算欄位屬於 行內計算。不管表示式裡用到的是單個欄位,還是多個欄位,使用的資料都在當前記錄行內。有行內計算,對應的也就有 跨行計算,如:第一名和第二名的差距;從 1 月到當前月份累計的銷售額。按照成績有序,才會有第一名、第二名的說法,累計操作同樣基於有序資料,從第幾個累加到第幾個,這些基於有序集合的計算,就屬於 有序計算。行內計算關心的是每條資料自身的情況,而跨行的有序計算則關心有序資料的變化情況。
一.相鄰記錄引用
簡單常見的有序計算是相鄰記錄引用,也就是在計算中要引用某種次序下的相鄰記錄。比如下面這些問題:
1、 股價每天的漲幅是多少(比上期)
按日期排序時,引用上一天的股價。
2、 前一天 + 當天 + 後一天的平均股價是多少(移動平均)
按日期排序時,引用前後兩天的股價。
3、 多支股票資料,計算每支股票內的每日漲幅(分組內的比上期)
按股票分組,組內按日期排序,引用上一天股價。
接下來通過這幾個例子研究下 SQL 如何實現這類有序計算。
1. SQL的解決方案
早期 SQL 的解決方案
早期的 SQL 沒有視窗函式,引用相鄰記錄的方法是用 JOIN 把相鄰記錄拼到同一行。
問題 1 寫出來是這樣的:
SELECT day, curr.price/pre.price rate
FROM (
SELECT day, price, rownum row1
FROM tbl ORDER BY day ASC) curr
LEFT JOIN (
SELECT day, price, rownum row2
FROM tbl ORDER BY day ASC) pre
ON curr.row1=pre.row2+1
即將本表和本表做 JOIN,把前一天和當天作為連線條件,這樣即可將前一天的股價和當天股價連線到同一行中,再用行內計算得到漲幅。一個很簡單的問題必須使用子查詢才能解決。
再看問題 2,計算股價的移動平均,(前一天 + 當天 + 後一天)/3,同樣是使用 JOIN 實現:
SELECT day, (curr.price+pre.price+after.price)/3 movingAvg
FROM (
SELECT day, price, rownum row1
FROM tbl ORDER BY day ASC) curr
LEFT JOIN (
SELECT day, price, rownum row2
FROM tbl ORDER BY day ASC) pre
ON curr.row1=pre.row2+1
LEFT JOIN (
SELECT day, price, rownum row3
FROM tbl ORDER BY day ASC) after
ON curr.row1=after.row3-1
多取一天,就多 JOIN 一個子查詢,試想,如果要計算前 10 天 ~ 後 10 天的移動平均,那需要寫 20 個 JOIN,這種語句能寫死人。
再看更復雜一些的問題 3,股價表裡有多支股票時,增加 code 欄位區分不同的股票,那它的漲幅就要限定在某支股票的分組內:
SELECT code, day ,currPrice/prePrice rate
FROM(
SELECT code, day, curr.price currPrice, pre.price prePrice
FROM (
SELECT code, day, price, rownum row1
FROM tbl ORDER BY code, day ASC) curr
LEFT JOIN (
SELECT code, day, price, rownum row2
FROM tbl ORDER BY code, day ASC) pre
ON curr.row1=pre.row2+1 AND curr.code=pre.code
)
這裡著重看兩個地方:單表排序時,一定要增加股票程式碼,形成組合排序 code,day,code 還必須要在前面,這不難理解,先把一支股票的資料放在一起,然後這支股票組內資料再按照日期排序;資料排序好了還不算完,連線條件裡也要加上股票程式碼相等,否則兩個相鄰的不同股票資料挨著,也會計算漲幅,但這是沒意義的髒資料。
引入視窗函式
從 2003 年起,SQL 標準中引入了視窗函式,帶來了序的概念。有序計算變得容易了許多。上面的三個例子寫起來就簡單多了。
問題 1,比上期。為了看清楚,把視窗函式拆成多行縮排,方便理解:
SELECT day, price /
LAG(price,1)
OVER (
ORDER BY day ASC
) rate
FROM tbl
LAG 函式實現引用前面的記錄。函式裡的參數列示找前面第 1 條的 price,OVER 是視窗函式 LAG 的子句(每個視窗函式都有 OVER 子句),它的作用是定義待分析的有序集合,這個例子很簡單,待分析集合按照日期有序。
問題 2,移動平均。可以用取前邊函式 LAG+ 取後面函式 LEAD 實現,但這裡用 AVG 函式更可取,它能支援一個範圍內(比如前後十條)的平均,LAG/LEAD 每次只能取到一個值:
SELECT price,
AVG(price) OVER (
ORDER BY day ASC
RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING
) movingAvg
FROM tbl;
這樣取前後 n 條也容易了,只要改變 RANGE BETWEEN 裡的範圍。
問題 3, 分組內的有序計算。每支股票的所有股價是一個分組,視窗函式對它也做了支援:
SELECT code, day, price /
LAG(price,1)
OVER (
PARTITION BY code
ORDER BY day ASC
) rate
FROM tbl
OVER 下的 PARTITION BY 子句描述了怎麼劃分分組,LAG 操作會限定在每個組內。這比之前的 JOIN 做法好了很多,描述分組很直觀。;而 JOIN 做法是對資料做組合排序,雖然實際上也是分組的效果,但不容易理解到位。
2. SPL的解決方案
問題前面解讀過了,先看下問題 1 的 SPL 指令碼:
A1: 從資料檔案匯入股市資料表
A2: 使用函式 sort 按日期排序後,計算每天股價與前日的漲幅。其中 price[-1] 表示前日的價格。函式 derive 用於為序表增加計算列。
SPL 同樣也支援從資料庫中讀取資料表,比如 A1 可以改為:
A1 的計算結果是一個序表,SPL中的序表是 有序集合,集合的成員是有次序的,這一點與Java等高階語言中的陣列類似。可以通過序號來訪問成員,是有序集合的基本功能。所以在處理有序運算問題時,有序集合具有天然的優勢。訪問前一個交易日的資料,對於 SQL 來說比較複雜,需要子查詢或者視窗函式的幫助。但是對於有序集合來說,只要訪問當前序號 -1 的成員就可以了。不但運算效率更高,理解起來也更加簡單。
我們繼續看一下問題 2 和問題 3 的解決方案。問題 2:
匯入股市資料表,並按日期排序。計算當日、前一日和後一日的股價平均值。其中 price[-1:1] 表示從前一日到後一日的股價。
問題 3:
匯入股市資料表,並按日期排序。使用函式 group 根據股票程式碼分組。每支股票分別計算當日與前一日股價的漲幅。其中符號 ~ 用來表示當前成員。
使用 SPL 來解決問題 2 和問題 3 時,仍然是通過相對的位置序號來訪問相鄰成員。解題思路與問題 1 類似,問題 2 只是從訪問前 1 天變成訪問連續 3 天,問題 3 引入了多支股票所以先按股票程式碼進行了分組。
二.序號定位
1. SQL的解決方案
有序集合裡找相鄰記錄,屬於相對位置定位,有時我們還會找絕對位置的記錄,比如計算每天股價與第一天上市價的漲跌差距:
SELECT day, price- FIRST_VALUE(price) OVER (ORDER BY day ASC) FROM tbl
或者,已經知道第 10 天是最高股價,計算出每天和它的差距:
SELECT day, price- NTH_VALUE(price, 10)OVER (ORDER BY day ASC) FROM tbl
再看複雜點的情況,要定位的序號事先未知,需要根據資料計算出來:
4、 股票按照股價排序,取出中間位置的股價(中位數)
先看簡單的單支股票的解法,按照股價排序後,中間位置還不知道在哪,這時得根據實際股票資料的數量算出中間位置:
SELECT *
FROM
SELECT day, price, ROW_NUMBER()OVER (ORDER BY day ASC) seq FROM tbl
WHERE seq=(
SELECT TRUNC((COUNT(*)+1)/2) middleSeq FROM tbl)
FROM 裡的子查詢用 ROW_NUMBER() 給每行生成序號,WHERE 裡的子查詢計算出中間序號。這個 SQL 裡有兩個注意事項,一是不能直接針對第一個子查詢內部過濾,因為 WHERE 裡不能使用同級 SELECT 中的計算欄位,這是 SQL 執行順序導致的;二是 WHERE 裡的子查詢結果一定是一行一列的單個值,這時能直接把它看成單個值和 seq 做等值比較。
計算多支股票中位數的 SQL 如下:
SELECT *
FROM
(SELECT code, day, price,
ROW_NUMBER() OVER ( PARTITION BY code ORDER BY day ASC)
FROM tbl) t1
WHERE seq=(
SELECT TRUNC((COUNT(*)+1)/2) middleSeq
FROM tbl t2
WHERE t1.code=t2.code
)
除了增加視窗函式裡的 PARTITION BY,還要注意計算中間位置時,查詢條件也要限定在一支股票內。
5、 每支股票最高價格日與前一天相比漲幅是多少
這個問題需要兩種排序方式組合起來定位,還是先看單支股票:
SELECT day, price, seq, rate
FROM (
SELECT day, price, seq,
price/LAG(price,1) OVER (ORDER BY day ASC) rate
FROM (
SELECT day, price,
ROW_NUMBER ()OVER (ORDER BY price DESC) seq
FROM tbl
)
)
WHERE seq=1
連續兩層子查詢都通過視窗函式給原始資料增加有用資訊,ROW_NUMBER 把價格從高到低標上序號,通過 LAG 計算出每天的漲幅,最後過濾出價格最高的一天就可以了(seq=1)。
注意過濾出最高價格不能先於漲幅的計算,最高價格的前一天還不知道在哪裡,先過濾掉,後面就算不出來漲幅了。
前面已經有幾個針對分組做有序計算的例子了,這個題就不給出最終答案了,讀者有興趣可以自己嘗試寫寫怎麼得出多支股票最高價時的漲幅。
2. SPL的解決方案
問題 4 要取出股價的中位數 ,SPL 指令碼如下:
匯入股市資料表,並按股價排序。根據股票程式碼分組後,每支股票分別取出股價中位數。
解題思路是,股票程式碼相同的股票分為一組並按股價排序,接下來每支股票的分組子集按照中間位置的序號訪問成員就可以了。
我們再來看一下問題 5,每支股票最高價格日與前一天相比漲幅是多少。SPL 指令碼如下:
匯入股市資料表,並按日期排序。根據股票程式碼分組。使用函式 pmax 定位股價最大值所在位置,再使用函式 calc 在指定位置上進行計算。
SPL 處理定位問題通常來說分為兩步:首先獲取成員或者滿足條件的位置(序號),然後我們就可以根據序號進行訪問或計算了。我們並不需要自己來實現定位,SPL 提供了很多定位函式,用於查詢成員或表示式在序表中的位置。
三.有序分組
1. SQL的解決方案
有序資訊還可以用於分組。看這個例子:
6、 一支股票最多連續上漲過幾天。
這個問題有點難想了。基本的思路是把按日期有序的股票記錄分成若干組,連續上漲的記錄分成同一組,也就是說,某天的股價比上一天是上漲的,則和上一天記錄分到同一組,如果下跌了,則開始一個新組。最後看所有分組中最大的成員數量,也就是最多連續上漲的天數。
這種分組比較特殊,和記錄的次序有關,而 SQL 裡只支援等值分組,就需要把這種有序分組轉換成常規的等值分組來實現。過程是這樣:
1) 按日期排序,用視窗函式取出每天的前一天股價;
2)對比,如果上漲了的標記為 0,下跌的標記為 1;
3)累加當前行以前的標記,累加的結果類似 0,0,1,1,1,1,2,2,3,3,3…,這些就是我們需要的組號了;
4)現在可以用 SQL 常規的等值分組了。
完整的 SQL 寫出來是這樣:
SELECT MAX(ContinuousDays)
FROM (
SELECT COUNT(*) ContinuousDays
FROM (
SELECT SUM(RisingFlag) OVER (ORDER BY day) NoRisingDays
FROM (
SELECT day, CASE WHEN price>
LAG(price) OVER (ORDER BY day) THEN 0 ELSE 1 END RisingFlag FROM tbl
)
) GROUP BY NoRisingDays
)
這個題已經不簡單了,巢狀了四層的子查詢。細追究下解題思路,就得說 SQL 語言與 JAVA/C 語言的不同特點,SQL 是集合化語言,提供的計算直接針對集合,沒有顯式可精細控制的迴圈操作,更沒有過程中的臨時變數可利用,這導致解決問題的思路和人的自然思路差異比較大,得變換思路,通過幾個規整的集合計算實現出等價效果;用非集合化的語言 JAVA 或 C,比較貼合自然思路,迴圈處理每個資料,過程中產生新組或加入舊組很直觀。當然 JAVA 等語言基本上沒有提供集合運算,也是各有特長。
合理的查詢需求在複雜程度上不會止步:
7、 連續上漲超過三天的股票有哪些?
這個問題題是有序分組 + 分組子集,最後再加個常規的分組、聚合值過濾(HAVING)。通過上個查詢的思路得到每支股票的所有上漲組,最外面套上分組運算得出每支股票的最大上漲天數,並用聚合後的條件運算 HAVING 過濾出上漲大於三天的:
SELECT code, MAX(ContinuousDays)
FROM (
SELECT code, NoRisingDays, COUNT(*) ContinuousDays
FROM (
SELECT code,
SUM(RisingFlag) OVER (PARTITION BY code ORDER BY day) NoRisingDays
FROM (
SELECT code, day,
CASE WHEN price>
LAG(price) OVER (PARTITION BY code ORDER BY day)
THEN 0 ELSE 1 END RisingFlag
FROM tbl
)
) GROUP BY NoRisingDays
)
GROUP BY code
HAVING MAX(ContinuousDays)>=3
這個 SQL 已經很難看懂了。
2. SPL的解決方案
我們先看一下問題 6, 一支股票最多連續上漲過幾天。SPL 指令碼如下:
匯入股市資料表,並按日期排序。使用函式 group 的選項 @o,根據股價是否上漲進行分組。分組時只和相鄰的對比,當股價是否上漲發生變化時產生新組。最後統計連續上漲的天數。
解題思路是,統計出股票每一次連續上漲的天數,再從中選出最長的一組。SPL 不僅語法簡單,更重要的是邏輯清晰,只要按思路順序編寫就可以了。SQL 和 SPL 處理有序分組問題的差別如此巨大,本質上因為 SQL 以無序集合為基礎,而 SPL 的序表是有序集合,更擅長於有序計算。除此以外,SPL 還提供了大量的函式,從而更加降低了使用難度。
問題 7,連續上漲超過三天的股票有哪些。SPL指令碼如下:
匯入股市資料表,並按日期排序。先按照股票程式碼分組,再按照問題 6 的方法,計算出每支股票連續上漲的最大天數,最後選出連續上漲超過 3 天的。
這個問題的 SQL 解決方案已經很難看懂了,但是 SPL 指令碼還是很簡單的。與問題 6 相比,僅僅是多了一個按股票程式碼分組的過程。SPL 的分組與 SQL 的分組有著本質上的區別。SQL 的分組除了只能得到分組彙總的結果,查詢時也只能選出分組時使用的欄位和聚合結果。而 SPL 使用直觀的記錄分組,比如本例中,將相同股票程式碼值的記錄分在一組,分組子集中保留了資料的全部資訊。正因為如此,我們才可以對這些分組子集進行下一步的計算。例如在本例中,我們可以對著每個分組子集再次進行有序分組。
總結
從上面的討論可以看出。沒有視窗函式 SQL 對有序運算極端不適應(目前還有些資料庫不支援視窗函式),理論上可以寫,但實際的麻煩程度基本上等同於不能用。在引入視窗函式後,有序計算得到了很好的改善,不過對於稍複雜情況還是相當麻煩。
這個原因在於 SQL 的理論基礎,也就是關係代數,是以無序集合作為基礎的,僅靠視窗函式這種打補丁的辦法並不能從根本上解決問題。
其實,計算機語言中的陣列(即集合)是天然有序的(有序號),在 Java/C++ 這些高階語言的思路下很容易理解和實現有序計算,但是這類語言的集合計算能力又比較弱,實現上面這些問題的程式碼也不短(雖然有序計算的解題思路難度並不大)。
esProc 的 SPL 可以很好地解決這一問題。esProc 是專業的資料計算引擎,基於有序集合設計,同時提供了完善的集合運算,相當於 Java 和 SQL 優勢的結合。在 SPL 的支援下,有序集合計算會非常容易。
SPL 中提供了跨行引用的語法,也支援有序分組等運算,有了這些後,上面那些問題只要按自然思維去組織計算邏輯,一行程式碼就能優雅地寫出來。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31543054/viewspace-2748320/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- SQL 有序計算SQL
- SQL Server中的集合運算: UNION, EXCEPT和INTERSECTSQLServer
- SQL位運算SQL
- matlab影像點運算 對比度增強 對比度拉伸 灰度變換Matlab
- oracle中字串的大小比較,字串與數字的比較和運算Oracle字串
- sqlserver 檢視和sql語句的效率對比SQLServer
- ORACLE和SQL SERVER,DB2對比OracleSQLServerDB2
- Oracle和SQL對比語法總結OracleSQL
- MySQL的四種運算子(算術運算子、比較運算子、邏輯運算子和位運算子)MySql
- 字串拼接運算比較字串
- MySql和SQL Server資料型別 對比MySqlServer資料型別
- 詳解 SQL 集合運算SQL
- 有序佇列比較佇列
- SQL與MongoDB的詳細對比SQLMongoDB
- 重要 | Spark和MapReduce的對比,不僅僅是計算模型?Spark模型
- 位運算與SQL實現SQL
- 【開發篇sql】 條件和表示式(五) 集合運算SQL
- Go 的記憶體對齊和指標運算詳解和實踐Go記憶體指標
- PSP個人專案耗時對比記錄表:四則運算
- Git和SVN的對比Git
- 簡單對比MySQL和Oracle中的一個sql解析細節MySqlOracle
- Linux 比較判斷運算(if else)Linux
- SQL Server 2005和Oracle高可用性對比SQLServerOracle
- 運維自動化工具對比運維
- Go 與 C++ 的對比和比較GoC++
- Python 和 Ruby 的對比Python
- java 和 Ruby On Rails的對比JavaAI
- redis和memcache的對比——配置Redis
- truncate 和 delete 的效能對比delete
- 有同也有異,對比BAT的運維文化BAT運維
- 結對專案四則運算
- WinRunner和QTP對比QT
- 對比Javascript和TypeScriptJavaScriptTypeScript
- redux 和 mobX對比Redux
- Django 和 struts 對比Django
- Mongo和Couch對比Go
- vite和webpack對比ViteWeb
- TCP和UDP對比TCPUDP