PL/SQL或替代方案的数值优化

Mat*_*ham 6 oracle optimization performance numerical plsql

我们需要做一些计算繁重的工作来连接Oracle数据库.到目前为止,我们已经在PL/SQL中进行了数值计算,并且很大程度上缺乏性能.

现在我们正在实施一个多级库存模型(https://www.researchgate.net/publication/222409130_Evaluation_of_time-varying_availability_in_multi-echelon_spare_parts_systems_with_passivation)由于问题的严重性,我更关注性能.

我用三种语言实现了部分算法:Fortran(90-2008符合gfortran),Excel中的VBA和PL/SQL,并围绕它进行了一百万次调用测试循环.即使使用binary_double数据类型和本机编译PLSQL_CODE_TYPE=NATIVE(两者都会导致改进),下面的测试代码仍然需要37秒才能运行(Oracle XE 11.2).相比之下,VBA在同一硬件上占用16s和Fortran 1.6s.

虽然要求性能接近Fortran数字可能太过分了(虽然这显然是非常需要的)但我很惊讶即使是不起眼的VBA也会执行PL/SQL.

所以我的问题有两个部分:

  1. 还有什么我可以尝试在Oracle方面进一步提高性能吗?
  2. 如果我们考虑摆脱PL/SQL,我们应该考虑哪些替代方案来连接Oracle数据库?虽然接近Fortran速度的性能提升将使我们考虑在应用程序的交互式部分中引入一些繁重的工作,但是繁重的工作将以类似批处理的方式被称为相对较少.我首选的数值工作语言仍然是Fortran,因为它易于实现带有屏蔽的多维数组.

此外,虽然我不是直接批评我的源代码本身,但如果有人能发现任何我可以合并的明显优化,我将不胜感激.

该函数timeebo是测试函数,我select timeebo from dual;在SQL Developer中使用一个简单的函数调用它.

create or replace FUNCTION gammln(
    x IN binary_double)
  RETURN binary_double
IS
  --Lanczos' approximation to the Log Gamma function
  gammln binary_double;
  ser binary_double;
  tmp binary_double;
BEGIN
  tmp := x + 5.5;
  tmp :=(x + 0.5) * Ln(tmp) - tmp;
  ser := 1.000000000190015;
  ser := ser + 76.18009172947146 /(x + 1.0) ;
  ser := ser - 86.50532032941677 /(x + 2.0) ;
  ser := ser + 24.01409824083091 /(x + 3.0) ;
  ser := ser - 1.231739572450155 /(x + 4.0) ;
  ser := ser + 1.208650973866179E-03 /(x + 5.0) ;
  ser := ser - 5.395239384953E-06 /(x + 6.0) ;
  RETURN tmp + Ln(2.5066282746310005 * ser / x) ;
END;
/
CREATE OR REPLACE FUNCTION PoissonDist(
    k      IN INTEGER,
    lambda IN binary_double)
  RETURN binary_double
IS
BEGIN
  RETURN Exp((k * Ln(lambda)) - lambda - gammln(k + 1)) ;
END;
/
CREATE OR REPLACE FUNCTION EBO(
    stock    IN pls_integer,
    pipeline IN binary_double,
    accuracy IN binary_double DEFAULT 0.0000000001)
  RETURN binary_double
IS
  i pls_integer;
  EBO binary_double;
  term binary_double;
  temp binary_double;
  PoissonVal binary_double;
  peaked BOOLEAN; --Flag the Poisson curve as having peaked
BEGIN
  EBO        := 0.0;
  IF(pipeline = 0.0) THEN
    RETURN EBO;
  END IF;
--Initialise
i            := 1;
peaked       := false;
PoissonVal   := PoissonDist(stock + 1, pipeline) ;         --Get p() value
IF(PoissonVal < accuracy AND floor(pipeline) > stock) THEN --If p() is very
  -- small...
  i          := floor(pipeline)   - stock;         --Revise i to just below peak of Poisson curve
  PoissonVal := PoissonDist(stock + i, pipeline) ; --Get p() value close to
  -- peak
  temp := PoissonVal *(pipeline / CAST(stock + i + 1 AS binary_double)) ; --
  -- Store poisson value just above peak
  LOOP
    term       := CAST(i AS binary_double) * PoissonVal;
    EBO        := EBO                         + term;
    i          := i                           - 1; --Work backwards
    PoissonVal := PoissonVal                  *(CAST(stock + i + 1 AS DOUBLE
    PRECISION)                                / pipeline) ; --Revise Poisson
    -- value for next time
    EXIT
  WHEN(term < accuracy OR i = 0) ;
  END LOOP;
  i          := 1 + floor(pipeline) - stock;
  PoissonVal := temp;
  peaked     := true;
END IF;
LOOP
  term       := CAST(i AS binary_double) * PoissonVal;
  EBO        := EBO                         + term;
  i          := i                           + 1;
  PoissonVal := PoissonVal                  *(pipeline / CAST(stock + i AS
  binary_double)) ; --Revise Poisson value for next time
  IF(CAST(stock + i AS binary_double) > pipeline) THEN
    peaked                              := true;
  END IF;
  EXIT
WHEN(term < accuracy AND peaked) ;
END LOOP;
IF(EBO < accuracy) THEN
  EBO := 0.0;
END IF;
RETURN EBO;
END;
/
CREATE OR REPLACE FUNCTION timeebo
  RETURN binary_double
IS
  i pls_integer;
  EBOVal binary_double;
  acc binary_double;
BEGIN
  acc := 0.0;
  FOR i IN 1..1000000
  LOOP
    EBOVal := EBO(500, CAST(i AS binary_double) / 1000.0) ;
    acc    := acc                                + EBOVal;
  END LOOP;
RETURN acc;
END;
Run Code Online (Sandbox Code Playgroud)

小智 3

这不是OP问题的答案(但它是一个“概念证明”,展示了如何在普通SQL中完成他在PL/SQL中所做的事情)。我只使用答案格式,因为我下面要做的事情不适合评论。

OP 要求了解如何在纯 SQL 中将无限级数求和到所需的精度。我举两个例子。

第一个例子

具有正项的无穷级数:e = sum [ j = 0 无穷大 ] ( 1 /阶乘( j ) )。从 0 到 n 的总和的误差上限是添加到级数中的最后一项。下面的递归查询(需要 Oracle 11.1 或更高版本 - 实际上是我编写的 11.2,声明中包含列名,但可以轻松更改为 11.1)计算 e 的值,精确到小数点后 38 位(最大精度)在 Oracle 中可用)。e的反阶乘级数收敛得非常快;这只需要 35 个步骤,在我的旧家用电脑(只是一台带有键盘的大型戴尔平板电脑)上运行只需不到 0.001 秒。

编辑: 呃!只有我可以发布e = 3.71828的内容!即使在递归查询中我添加了所有项(包括 1/0!),我还是从 1 而不是 0 开始求和。(现在已更正,但在更正之前就出现了此错误。)

with 
     rec ( j, s, next_term, err ) as (
       select  0, 0, 1, 2
         from  dual
       union all
       select  j+1, s + next_term, next_term/(j+1), next_term
         from  rec
         where err > power(10, -38) and j < 1000000000
     )
select max(j) as steps, round(max(s), 38) as e 
from   rec
;

STEPS                                         E
-----  ----------------------------------------
   35  2.71828182845904523536028747135266249776
Run Code Online (Sandbox Code Playgroud)

第二个例子

好的,现在让我们采用一个交替级数(其中最后一项的绝对值始终是误差的上限),然后让我们采用一个非常慢收敛的级数:

ln( 2 ) =总和 [ j = 1 无穷大 ] ( (-1)^( j - 1) / j )

下面的查询计算 ln(2),精确到小数点后五位;这里我们事先知道我们需要 100,000 步,而我的机器上的计算大约花费了 1.1 秒。(但请记住,这是一个非常缓慢收敛的系列。)

with
     rec ( j, s, sgn ) as (
       select  0, 0, 1
         from  dual
       union all
       select  j+1, s + sgn / (j+1), -sgn
         from  rec
         where j <= 100000
     )
select 100000 as steps, round(s, 5) as ln_2
from   rec
where  j = 100000
;

 STEPS     LN_2
------  -------
100000  0.69314
Run Code Online (Sandbox Code Playgroud)