問題:計算某個數(shù)字列的累乘積。其操作方式與“計算累計和”相似,只是使用乘法而不是加法。
解決方案
作為例子,本解決方案中都計算職員工資的累乘積。雖然工資的累乘積沒有多大用處,然而可以很容易地把該技巧用于其他更有用的領域。
DB2和Oracle
使用窗口函數(shù)SUM OVER,用對數(shù)相加來模擬乘法操作:
1 select empno,ename,sal,
2 exp(sum(ln(sal))over(order by sal,empno)) as running_prod
3 from emp
4 where deptno = 10
EMPNO ENAME SAL RUNNING_PROD
----- ---------- ---- --------------------
7934 MILLER 1300 1300
7782 CLARK 2450 3185000
7839 KING 5000 15925000000
在SQL中,對小于等于0的值取對數(shù)是無效的。如果表中包含這樣的值,一定要避免把這些無效的值傳遞給SQL的LN函數(shù)。為了增加可讀性,該解決方案并沒有對無效值和NULL值采取防范措施,但自己編寫代碼時,一定要考慮是否需要這種預防。如果一定要用到負值和0值,那么這種解決方案不合適。
Oracle獨有的另一種解決方案是使用Oracle Database 10g新引入的MODEL子句。在下面的例子中,每個SAL都是負數(shù),這表明累乘積允許出現(xiàn)負值:
1 select empno, ename, sal, tmp as running_prod
2 from (
3 select empno,ename,-sal as sal
4 from emp
5 where deptno=10
6 )
7 model
8 dimension by(row_number()over(order by sal desc) rn )
9 measures(sal, 0 tmp, empno, ename)
10 rules (
11 tmp[any] = case when sal[cv()-1] is null then sal[cv()]
12 else tmp[cv()-1]*sal[cv()]
13 end
14 )
EMPNO ENAME SAL RUNNING_PROD
----- ---------- ---- --------------------
7934 MILLER -1300 -1300
7782 CLARK -2450 3185000
7839 KING -5000 -15925000000
MySQL、PostgreSQL和SQL Server
還可以使用對數(shù)相加的方法,但這些平臺并不支持窗口函數(shù),因此用標量子查詢取而代之:
1 select e.empno,e.ename,e.sal,
2 (select exp(sum(ln(d.sal)))
3 from emp d
4 where d.empno <= e.empno
5 and e.deptno=d.deptno) as running_prod
6 from emp e
7 where e.deptno=10
EMPNO ENAME SAL RUNNING_PROD
----- ---------- ---- --------------------
7782 CLARK 2450 2450
7839 KING 5000 12250000
7934 MILLER 1300 15925000000
SQL Server用戶使用LOG代替LN。
討論
除了MODEL子句方案(僅對Oracle Database 10g或更高版本可用)之外,所有解決方案都利用了乘法運算的特性,按下列步驟用加法進行計算:
1. 計算各自的自然對數(shù)
2. 計算這些對數(shù)的和
3. 對結果進行數(shù)學常量e的冪運算(使用EXP函數(shù))
當采用這種方法時,需要注意,對于0值和負值,這種方法不可行,因為任何小于等于0的值都超出了SQL對數(shù)的定義域。
DB2和Oracle
有關窗口函數(shù)SUM OVER的功能,請參閱“生成累計和”一節(jié)。
對于Oracle Database 10g或更高版本,可以使用MODEL子句生成累乘積。同時使用MODEL子句及窗口函數(shù)ROW_NUMBER,很容易就能訪問前面的行。可以像訪問數(shù)組一樣訪問MEASURES列表中的每一項。然后,可以使用DIMENSIONS列表中的項(由ROW_NUMBER返回的值,別名RN)搜索該數(shù)組:
select empno, ename, sal, tmp as running_prod,rn
from (
select empno,ename,-sal as sal
from emp
where deptno=10
)
model
dimension by(row_number()over(order by sal desc) rn )
measures(sal, 0 tmp, empno, ename)
rules ()
EMPNO ENAME SAL RUNNING_PROD RN
----- ---------- ---------- ------------ ----------
7934 MILLER -1300 0 1
7782 CLARK -2450 0 2
7839 KING -5000 0 3
觀察一下,會發(fā)現(xiàn)SAL[1]的值為-1300。由于數(shù)字逐一連續(xù)遞增、沒有間隙,所以可以通過減1來引用前一行。RULES子句如下:
rules (
tmp[any] = case when sal[cv()-1] is null then sal[cv()]
else tmp[cv()-1]*sal[cv()]
end
)
它使用內置操作符ANY處理每一行,而并未進行硬編碼。這個例子中ANY的值分別為1、2和3。把TMP[n]初始化為0。通過計算相應SAL行的當前值(函數(shù)CV返回當前值),可以給TMP[n]指定一個值。把TMP[1]初始化為0,把SAL[1]初始化為-1300。SAL[0]沒有值,所以把TMP[1]設置為SAL[1]。在設置了TMP[1]之后,下一行就是TMP[2]。計算第一個SAL[1](由于ANY的當前值是2,因此SAL[CV()-1]的值是SAL[1])。SAL[1]不為空,而且等于-1300,因此把TMP[2]設置為TMP[1]和SAL[2]的乘積。所有行都進行上述操作。
MySQL、PostgreSQL和SQL Server
有關MySQL、PostgreSQL和SQL Server解決方案所采用的子查詢方法的說明,請參閱本章第7.6節(jié)。
要注意,基于子查詢解決方案的輸出與Oracle和DB2解決方案的輸出有少許差別,其原因來自EMPNO比較(它們按不同的順序計算累乘積)。與累計和一樣,其總數(shù)也是由標量子查詢的謂詞驅動的;在該解決方案中,行是按EMPNO排序的,而對于Oracle/DB2 解決方案,行是按SAL排序的。