问题:计算某个数字列的累乘积。其操作方式与“计算累计和”相似,只是使用乘法而不是加法。
解决方案
作为例子,本解决方案中都计算职员工资的累乘积。虽然工资的累乘积没有多大用处,然而可以很容易地把该技巧用于其他更有用的领域。
DB2和Oracle
使用窗口函数SUM OVER,用对数相加来模拟乘法操作:
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的值取对数是无效的。如果表中包含这样的值,一定要避免把这些无效的值传递给SQL的LN函数。为了增加可读性,该解决方案并没有对无效值和NULL值采取防范措施,但自己编写代码时,一定要考虑是否需要这种预防。如果一定要用到负值和0值,那么这种解决方案不合适。
Oracle独有的另一种解决方案是使用Oracle Database 10g新引入的MODEL子句。在下面的例子中,每个SAL都是负数,这表明累乘积允许出现负值:
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
还可以使用对数相加的方法,但这些平台并不支持窗口函数,因此用标量子查询取而代之:
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. 计算各自的自然对数
2. 计算这些对数的和
3. 对结果进行数学常量e的幂运算(使用EXP函数)
当采用这种方法时,需要注意,对于0值和负值,这种方法不可行,因为任何小于等于0的值都超出了SQL对数的定义域。
DB2和Oracle
有关窗口函数SUM OVER的功能,请参阅“生成累计和”一节。
对于Oracle Database 10g或更高版本,可以使用MODEL子句生成累乘积。同时使用MODEL子句及窗口函数ROW_NUMBER,很容易就能访问前面的行。可以像访问数组一样访问MEASURES列表中的每一项。然后,可以使用DIMENSIONS列表中的项(由ROW_NUMBER返回的值,别名RN)搜索该数组:
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
观察一下,会发现SAL[1]的值为-1300。由于数字逐一连续递增、没有间隙,所以可以通过减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行的当前值(函数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节。
要注意,基于子查询解决方案的输出与Oracle和DB2解决方案的输出有少许差别,其原因来自EMPNO比较(它们按不同的顺序计算累乘积)。与累计和一样,其总数也是由标量子查询的谓词驱动的;在该解决方案中,行是按EMPNO排序的,而对于Oracle/DB2 解决方案,行是按SAL排序的。