SQL 有序計算
什麼是有序計算
使用過 SQL 的朋友對計算欄位都不會陌生,比如 firstname+lastname,year(birthday),這些計算欄位屬於 行內計算,不管表示式裡用到的是單個欄位,還是多個欄位,使用的資料都在當前記錄行內。有行內計算,對應的也就有 跨行計算,如:第一名和第二名的差距;從 1 月到當前月份累計的銷售額。按照成績有序,才會有第一名、第二名的說法,累計操作同樣基於有序資料,從第幾個累加到第幾個,這些基於有序集合的計算,就屬於 有序計算。行內計算關心的是每條資料自身的情況,而跨行的有序計算則關心有序資料的變化情況。
相鄰記錄引用
簡單常見的有序計算是相鄰記錄引用,也就是在計算中要引用某種次序下的相鄰記錄。比如下面這些問題:
1、 股價每天的漲幅是多少(比上期)
按日期排序時,引用上一天的股價。
2、 前一天 + 當天 + 後一天的平均股價是多少(移動平均)
按日期排序時,引用前後兩天的股價。
3、 多支股票資料,計算每支股票內的每日漲幅(分組內的比上期)
按股票分組,組內按日期排序,引用上一天股價。
接下來通過這幾個例子研究下 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 做法是對資料做組合排序,雖然實際上也是分組的效果,但不容易理解到位。
序號定位
有序集合裡找相鄰記錄,屬於相對位置定位,有時我們還會找絕對位置的記錄,比如計算每天股價與第一天上市價的漲跌差距:
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)seq 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)。
注意過濾出最高價格不能先於漲幅的計算,最高價格的前一天還不知道在哪裡,先過濾掉,後面就算不出來漲幅了。
前面已經有幾個針對分組做有序計算的例子了,這個題就不給出最終答案了,讀者有興趣可以自己嘗試寫寫怎麼得出多支股票最高價時的漲幅。
有序分組
有序資訊還可以用於分組。看這個例子:
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 已經很難看懂了。
總結
從上面的討論可以看出。沒有視窗函式 SQL 對有序運算極端不適應(目前還有些資料庫不支援視窗函式),理論上可以寫,但實際的麻煩程度基本上等同於不能用。在引入視窗函式後,有序計算得到了很好的改善,不過對於稍複雜情況還是相當麻煩。
這個原因在於 SQL 的理論基礎,也就是關係代數,是以無序集合作為基礎的,僅靠視窗函式這種打補丁的辦法並不能從根本上解決問題。
其實,計算機語言中的陣列(即集合)是天然有序的(有序號),在 Java/C++ 這些高階語言的思路下很容易理解和實現有序計算,但是這類語言的集合計算能力又比較弱,實現上面這些問題的程式碼也不短(雖然有序計算的解題思路難度並不大)。
esProc 的 SPL 可以很好地解決這一問題。esProc 是專業的資料計算引擎,基於有序集合設計,同時提供了完善的集合運算,相當於 Java 和 SQL 優勢的結合。在 SPL 的支援下,有序集合計算會非常容易,比如上面的問題用 SPL 寫出來是這樣:
1、 T.sort(day).derive(price/price[-1]:rate)
2、 T.sort(day).derive(avg(price[-1:1]):movingAvg)
3、 T.sort(day).group(code).(~.derive(price/price[-1]:rate)).conj()
4、 T.sort(price).group(code).(~((~.len()+1)\2))
5、 T.sort(day).group(code).((p=~.pmax(price),~.calc(p,price/price[-1])))
6、 T.sort(day).group@o(price >price[-1]).max(~.len()))
7、 T.sort(day).group(code).select(~.group@o(price>price[-1]).max(~.len())>3).(code)
SPL 中提供了跨行引用的語法,也支援有序分組等運算,有了這些後,上面那些問題只要按自然思維去組織計算邏輯,一行程式碼就能優雅地寫出來。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31543054/viewspace-2737316/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- SQL 和 SPL 的有序運算對比SQL
- SQL 轉置計算SQL
- SQL 的後計算指令碼SQL指令碼
- SQL面試必考——計算留存率SQL面試
- SQL Server各種日期計算方法SQLServer
- [20211009]使用bash計算sql語句的sql_id.txtSQL
- SQL Server與雲端計算(下)WCSQLServer
- SQL Server與雲端計算(上)UHSQLServer
- [20191101]通過zsh計算sql語句的sql_id.txtSQL
- [20191011]通過bash計算sql語句的sql_id.txtSQL
- sql server 重新啟動計算機失敗SQLServer計算機
- Flink流計算中SQL表的概念和原理SQL
- 10個SQL技巧之三:進行總計算SQL
- 資料庫-SQL_duckdb向量化計算-vector資料庫SQL
- [20220104]檔案格式與sql_id計算.txtSQL
- 伍翀 :大資料實時計算Flink SQL解密大資料SQL解密
- SQL 如何計算每個分組的中位數SQL
- [20191012]使用bash從sql_id計算hash_value.txtSQL
- [20220111]該語句的sql_id如何計算的.txtSQL
- 使用CSS計數器美化有序列表CSS
- Serverless 解惑——函式計算如何訪問 SQL Server 資料庫Server函式SQL資料庫
- 通過sql 計算兩經緯度之間的距離SQL
- 使用SQL實現車流量的計算的示例程式碼SQL
- 5601. 設計有序流 (場景模擬)
- SQL能完成哪方面的計算?一文詳解關係代數和SQL語法SQL
- 奇思妙想的SQL|去重Cube計算最佳化新思路SQL
- [20200212]使用DBMS_SHARED_POOL.MARKHOT與sql的計算2.txtSQL
- [20200211]使用DBMS_SHARED_POOL.MARKHOT與sql_id的計算.txtSQL
- 位運算與SQL實現SQL
- leetcode.5601. 設計有序流(design-an-ordered-stream)LeetCode
- 使用CSS計數器美化數字有序列表CSS
- 本地計算、雲端計算、霧計算、邊緣計算有什麼區別?
- redis有序集合Redis
- [20211011]計算sql_id.sh指令碼的一些補充.txtSQL指令碼
- 邊緣計算、霧計算、雲端計算區別幾何?
- Pandas 基礎 (19) - 運算元據庫 (read_sql, to_sql)SQL
- 計算
- Sql語句實現不同記錄同一屬性列的差值計算SQL