【问题标题】:Function to Calculate Formula Without Using a For Loop in PL/SQL在 PL/SQL 中不使用 For 循环来计算公式的函数
【发布时间】:2020-04-09 04:07:43
【问题描述】:

对 PL/SQL 来说还是有点新,但基本上我正在尝试创建一个函数,该函数将根据一个人在过去 8 年中支付的金额来计算他们的分数。它使用每年的总和进行计算。公式为 (Year-1)/Year-2) * (Year-2/Year-3) * (Year3/Year4) 等等。更棘手的是,我需要跳过他们给出 0 的年份。

例如:

这是我目前的代码:

CREATE OR REPLACE FUNCTION formula
(idnum IN NUMBER)
RETURN NUMBER IS score NUMBER; 

-- Declare Variables
currentyear NUMBER := EXTRACT (YEAR FROM SYSDATE);
previousyear NUMBER := currentyear - 1;
yeareight NUMBER := currentyear - 8;
previoussum NUMBER := 0;
currentsum NUMBER := 0;
placeholder NUMBER := 0;
score NUMBER := 1;

BEGIN
-- Set Score to 0 if no history of payments in the last 8 years
SELECT NVL(SUM(amount), 0)
   INTO currentsum
   FROM moneytable g
   WHERE g.id_number = idnum
         AND g.fiscal_year BETWEEN yeareight AND previousyear;    

IF currentsum = 0 THEN score := 0;
  ELSE 
-- Loop to calculate Score
-- Score formula is (Year-1/Year -2) * (Year-2/Year-3) and so on for the last 8 years
-- Zeroes ignored for above calculations  
-- Score defaults to 1 if only one year has any gifts
FOR counter IN 1..8
  LOOP

  currentyear := currentyear - 1;
  placeholder := 0;

  SELECT NVL(SUM(amount), 0)
  INTO currentsum
  FROM moneytable g
  WHERE g.id_number = idnum
        AND g.fiscal_year = currentyear;

 IF currentsum = 0 THEN CONTINUE;
 ELSE placeholder := previoussum / currentsum; END IF;

 previoussum := currentsum;

 IF currentsum > 0 AND placeholder > 0 THEN score := score * placeholder; END IF;

END LOOP; 
END IF;
RETURN score;

END;

它有效并且给出了正确的分数,但是如果我尝试一次为几个人运行它,它会运行得非常慢。有没有更高效、优化的方法来创建这个函数?

【问题讨论】:

    标签: sql oracle function for-loop plsql


    【解决方案1】:

    UNPIVOT摆脱空虚的岁月

    select * from tab
      UNPIVOT (  value                             
                FOR year  IN                             
                (YEAR1, YEAR2, YEAR3, YEAR4, YEAR5, YEAR6, YEAR7, YEAR8) 
              )
    where value != 0
    order by 1,2;
    
    NAME YEAR       VALUE
    ---- ----- ----------
    Jane YEAR1         10
    Jane YEAR3         20
    Jane YEAR4         50
    Jane YEAR7         30
    Jane YEAR8         20
    Rob  YEAR2         10
    Rob  YEAR4         20
    ...
    

    然后使用 LEAD 聚合函数计算系数(默认使用与行中相同的 VALUE 以忽略最后一个系数 - 将其设置为 1)。

    with formula as (select * from tab
      UNPIVOT (  value                             
                FOR year  IN                             
                (YEAR1, YEAR2, YEAR3, YEAR4, YEAR5, YEAR6, YEAR7, YEAR8) 
              )
    where value != 0)
    select NAME, YEAR, VALUE,
    VALUE / lead(value,1,VALUE) over (partition by NAME order by YEAR) as koeff
    from formula
    order by 1,2;
    
    NAME YEAR       VALUE      KOEFF
    ---- ----- ---------- ----------
    Jane YEAR1         10 ,5        
    Jane YEAR3         20 ,4        
    Jane YEAR4         50 1,66666667
    Jane YEAR7         30 1,5       
    Jane YEAR8         20          1
    Rob  YEAR2         10 ,5        
    Rob  YEAR4         20 ,2 
    ...
    

    在最后一步中,使用this trick 计算系数的聚合乘积

    with formula as (select * from tab
      UNPIVOT (  value                             
                FOR year  IN                             
                (YEAR1, YEAR2, YEAR3, YEAR4, YEAR5, YEAR6, YEAR7, YEAR8) 
              )
    where value != 0),
    formula2 as (
    select NAME, YEAR, VALUE,
    VALUE / lead(value,1,VALUE) over (partition by NAME order by YEAR) as koeff
    from formula)
    select name, 
    round(EXP(SUM(LN(koeff))),6) score
    from formula2
    group by name
    order by 1 ;
    
    
    NAME      SCORE
    ---- ----------
    Jane ,5        
    Rob  ,1        
    Tom  ,2 
    

    测试数据

    create table tab as
    select 'Tom' name, 0 year1, 0 year2, 0 year3, 10 year4, 20 year5, 30 year6, 40 year7, 50 year8 
    from dual union all
    select 'Jane' name, 10 year1, 0 year2, 20 year3, 50 year4, 0 year5, 0 year6, 30 year7, 20 year8 
    from dual union all
    select 'Rob' name, 0 year1, 10 year2, 0 year3, 20 year4, 0 year5, 0 year6, 0 year7, 100 year8 
    from dual;
    

    【讨论】:

      【解决方案2】:

      也许最好调用过去 8 年的单个查询并将结果放在一个数组中,然后循环它,而不执行 8 个查询:

      DECLARE
        TYPE arrayofnumbers IS TABLE OF NUMBER(11);
        sums arrayofnumbers;
      BEGIN
        SELECT NVL(SUM(amount), 0)
        INTO sums
        FROM moneytable g
          WHERE g.id_number = idnum AND g.fiscal_year between currentyear and currentyear+7;
      
       FOR i IN 1 .. sums.count
        LOOP
          -- other code
          dbms_output.put_line(sums(i));
      
        END LOOP;
      END;
      

      【讨论】:

      • 我要感谢大家的意见,但我将此标记为正确答案,因为它最接近我的需要。谢谢麦克斯!我收到“不允许的集合类型错误”,所以我用“IS ARRAY(8) OF”和一个光标替换了“IS TABLE OF”,它工作得很好!它的运行速度比我原来的解决方案快得令人难以置信。
      【解决方案3】:

      您想要第一个非零值除以最后一个非零值。那将是:

      select (case when year1 <> 0 then year1
                   when year2 <> 0 then year2
                   when year3 <> 0 then year3
                   when year4 <> 0 then year4
                   when year5 <> 0 then year5
                   when year6 <> 0 then year6
                   when year7 <> 0 then year7
                   when year8 <> 0 then year8
              end) /
             (case when year8 <> 0 then year8
                   when year7 <> 0 then year7
                   when year6 <> 0 then year6
                   when year5 <> 0 then year5
                   when year4 <> 0 then year4
                   when year3 <> 0 then year3
                   when year2 <> 0 then year2
                   when year1 <> 0 then year1
              end)  
      

      【讨论】:

        【解决方案4】:

        只查询一次表并按年份分组结果。将它们按降序编号并循环八行。它可以在 SQL 中完成,也可以在函数中循环遍历分组行。 SQL 示例:

        dbfiddle

        with y(rn, amt) as (
            select row_number() over (order by fiscal_year desc), sum(amount)
              from moneytable g  
              where id_number = 1
                and fiscal_year between extract (year from sysdate) - 8 
                                    and extract(year from sysdate) - 1
              group by fiscal_year),
          c(rn, amt, prev, ret) as (
            select rn, amt, amt, 1 from y where rn = 1
            union all
            select y.rn, y.amt, c.amt, (c.amt/y.amt)*ret from c join y on y.rn = c.rn + 1)
        select ret from c where rn = (select max(rn) from c)
        

        【讨论】:

          猜你喜欢
          • 2013-07-21
          • 2013-03-29
          • 2017-07-30
          • 1970-01-01
          • 2020-07-16
          • 1970-01-01
          • 2016-08-03
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多