問(wèn)題:計(jì)算某個(gè)數(shù)字列的累乘積。其操作方式與“計(jì)算累計(jì)和”相似,只是使用乘法而不是加法。
解決方案
作為例子,本解決方案中都計(jì)算職員工資的累乘積。雖然工資的累乘積沒(méi)有多大用處,然而可以很容易地把該技巧用于其他更有用的領(lǐng)域。
DB2和Oracle
使用窗口函數(shù)SUM OVER,用對(duì)數(shù)相加來(lái)模擬乘法操作:
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中,對(duì)小于等于0的值取對(duì)數(shù)是無(wú)效的。如果表中包含這樣的值,一定要避免把這些無(wú)效的值傳遞給SQL的LN函數(shù)。為了增加可讀性,該解決方案并沒(méi)有對(duì)無(wú)效值和NULL值采取防范措施,但自己編寫代碼時(shí),一定要考慮是否需要這種預(yù)防。如果一定要用到負(fù)值和0值,那么這種解決方案不合適。
Oracle獨(dú)有的另一種解決方案是使用Oracle Database 10g新引入的MODEL子句。在下面的例子中,每個(gè)SAL都是負(fù)數(shù),這表明累乘積允許出現(xiàn)負(fù)值:
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
還可以使用對(duì)數(shù)相加的方法,但這些平臺(tái)并不支持窗口函數(shù),因此用標(biāo)量子查詢?nèi)《?
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子句方案(僅對(duì)Oracle Database 10g或更高版本可用)之外,所有解決方案都利用了乘法運(yùn)算的特性,按下列步驟用加法進(jìn)行計(jì)算:
1. 計(jì)算各自的自然對(duì)數(shù)
2. 計(jì)算這些對(duì)數(shù)的和
3. 對(duì)結(jié)果進(jìn)行數(shù)學(xué)常量e的冪運(yùn)算(使用EXP函數(shù))
當(dāng)采用這種方法時(shí),需要注意,對(duì)于0值和負(fù)值,這種方法不可行,因?yàn)槿魏涡∮诘扔?的值都超出了SQL對(duì)數(shù)的定義域。
DB2和Oracle
有關(guān)窗口函數(shù)SUM OVER的功能,請(qǐng)參閱“生成累計(jì)和”一節(jié)。
對(duì)于Oracle Database 10g或更高版本,可以使用MODEL子句生成累乘積。同時(shí)使用MODEL子句及窗口函數(shù)ROW_NUMBER,很容易就能訪問(wèn)前面的行。可以像訪問(wèn)數(shù)組一樣訪問(wèn)MEASURES列表中的每一項(xiàng)。然后,可以使用DIMENSIONS列表中的項(xiàng)(由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
觀察一下,會(huì)發(fā)現(xiàn)SAL[1]的值為-1300。由于數(shù)字逐一連續(xù)遞增、沒(méi)有間隙,所以可以通過(guò)減1來(lái)引用前一行。RULES子句如下:
rules (
tmp[any] = case when sal[cv()-1] is null then sal[cv()]
else tmp[cv()-1]*sal[cv()]
end
)
它使用內(nèi)置操作符ANY處理每一行,而并未進(jìn)行硬編碼。這個(gè)例子中ANY的值分別為1、2和3。把TMP[n]初始化為0。通過(guò)計(jì)算相應(yīng)SAL行的當(dāng)前值(函數(shù)CV返回當(dāng)前值),可以給TMP[n]指定一個(gè)值。把TMP[1]初始化為0,把SAL[1]初始化為-1300。SAL[0]沒(méi)有值,所以把TMP[1]設(shè)置為SAL[1]。在設(shè)置了TMP[1]之后,下一行就是TMP[2]。計(jì)算第一個(gè)SAL[1](由于ANY的當(dāng)前值是2,因此SAL[CV()-1]的值是SAL[1])。SAL[1]不為空,而且等于-1300,因此把TMP[2]設(shè)置為TMP[1]和SAL[2]的乘積。所有行都進(jìn)行上述操作。
MySQL、PostgreSQL和SQL Server
有關(guān)MySQL、PostgreSQL和SQL Server解決方案所采用的子查詢方法的說(shuō)明,請(qǐng)參閱本章第7.6節(jié)。
要注意,基于子查詢解決方案的輸出與Oracle和DB2解決方案的輸出有少許差別,其原因來(lái)自EMPNO比較(它們按不同的順序計(jì)算累乘積)。與累計(jì)和一樣,其總數(shù)也是由標(biāo)量子查詢的謂詞驅(qū)動(dòng)的;在該解決方案中,行是按EMPNO排序的,而對(duì)于Oracle/DB2 解決方案,行是按SAL排序的。