重构 Oracle 存储过程以使用 BULK COLLECT

Posted

技术标签:

【中文标题】重构 Oracle 存储过程以使用 BULK COLLECT【英文标题】:Refactor Oracle stored procedure to use BULK COLLECT 【发布时间】:2019-12-27 20:39:18 【问题描述】:

我有一个跟踪 Oracle 12c 数据库中每小时数据的数据库视图。数据是这样排列的,一天中的每个小时都有列。

SALES_DATE  | LOCATION_CODE | HE1_SALES | HE2_SALES | ... | HE24_SALES
_________________________________________________________________________
12/27/2019  | ABCD          |    40     |    50     | ... |    60
12/26/2019  | ABCD          |    51     |    64     | ... |    68
12/27/2019  | ABCG          |    53     |    54     | ... |    50
12/26/2019  | ABCG          |    45     |    47     | ... |    52

我有一个存储过程,它查看过去 10 年,并尝试查找每小时模式与用户定义的日期最相似的日期。这是通过获取每个小时的差异并将其添加到总数中来实现的。总差异在 50 以内的任何日期都将在列表中返回(因此结果集通常非常小)。然后它将结果放在一个表格中,以便用户稍后可以回来查看它们,直到他们再次运行该过程(当他们查看数据时,它首先显示最相似的排序,基于该“接近度”)。以下是当前形式的程序:

CREATE OR REPLACE PROCEDURE MYDB.COMPARE_SALES_SP (
    p_compare_date       IN DATE,
    p_location_code      IN VARCHAR2,
    p_userid             IN VARCHAR2,
    p_message            OUT VARCHAR2)
IS
    TYPE SalesArray IS TABLE OF NUMBER
        INDEX BY PLS_INTEGER;

    v_hours_count     INTEGER;
    v_difference      NUMBER;
    v_compare_sales   SalesArray;
    v_curr_sales      SalesArray;
BEGIN
    DELETE FROM MYDB.SALES_ANALYSIS
     WHERE userid = p_userid;
    IF TRUNC (SYSDATE) = TRUNC (p_compare_date)
    THEN
        v_hours_count := MYDB.F_HOUR_ENDING_NUMBER (SYSDATE);
    ELSE
        v_hours_count := 24;
    END IF;

    SELECT HE1_SALES, HE2_SALES, HE3_SALES, HE4_SALES, HE5_SALES, HE6_SALES,
           HE7_SALES, HE8_SALES, HE9_SALES, HE10_SALES, HE11_SALES, HE12_SALES,
           HE13_SALES, HE14_SALES, HE15_SALES, HE16_SALES, HE17_SALES, HE18_SALES,
           HE19_SALES, HE20_SALES, HE21_SALES, HE22_SALES, HE23_SALES, HE24_SALES
      INTO v_compare_sales (1), v_compare_sales (2), v_compare_sales (3), v_compare_sales (4),
           v_compare_sales (5), v_compare_sales (6), v_compare_sales (7), v_compare_sales (8),
           v_compare_sales (9), v_compare_sales (10), v_compare_sales (11), v_compare_sales (12),
           v_compare_sales (13), v_compare_sales (14), v_compare_sales (15), v_compare_sales (16),
           v_compare_sales (17), v_compare_sales (18), v_compare_sales (19), v_compare_sales (20),
           v_compare_sales (21), v_compare_sales (22), v_compare_sales (23), v_compare_sales (24)
      FROM MYDB.SALES_BY_DAY
     WHERE reading_date = TRUNC (p_compare_date) AND location_code = p_location_code;

    FOR i
        IN (SELECT *
              FROM MYDB.SALES_BY_DAY sd
             WHERE sd.READING_DATE > (SYSDATE - 3652)
               AND sd.READING_DATE != TRUNC(p_compare_date)
               AND location_code = p_location_code)
    LOOP
        v_difference := 0;

        SELECT i.HE1_SALES, i.HE2_SALES, i.HE3_SALES, i.HE4_SALES, i.HE5_SALES, i.HE6_SALES,
               i.HE7_SALES, i.HE8_SALES, i.HE9_SALES, i.HE10_SALES, i.HE11_SALES, i.HE12_SALES,
               i.HE13_SALES, i.HE14_SALES, i.HE15_SALES, i.HE16_SALES, i.HE17_SALES, i.HE18_SALES,
               i.HE19_SALES, i.HE20_SALES, i.HE21_SALES, i.HE22_SALES, i.HE23_SALES, i.HE24_SALES
          INTO v_curr_sales (1), v_curr_sales (2), v_curr_sales (3), v_curr_sales (4),
               v_curr_sales (5), v_curr_sales (6), v_curr_sales (7), v_curr_sales (8),
               v_curr_sales (9), v_curr_sales (10), v_curr_sales (11), v_curr_sales (12),
               v_curr_sales (13), v_curr_sales (14), v_curr_sales (15), v_curr_sales (16),
               v_curr_sales (17), v_curr_sales (18), v_curr_sales (19), v_curr_sales (20),
               v_curr_sales (21), v_curr_sales (22), v_curr_sales (23), v_curr_sales (24)
          FROM DUAL;

        FOR j IN 1 .. v_hours_count
        LOOP
            v_difference := v_difference + ABS (v_compare_sales (j) - v_curr_sales (j));
        END LOOP;
        IF (v_difference < 50)
        THEN
            INSERT INTO MYDB.SALES_ANALYSIS (READING_DATE, location_code, USERID, PROXIMITY)
                VALUES (i.READING_DATE, i.location_code, p_userid, v_difference);
        END IF;

    END LOOP;
    COMMIT;
    p_message := 'Sales analysis successful. Please review the results';
EXCEPTION
    WHEN OTHERS
    THEN
        ROLLBACK;
        p_message := 'Sales analysis was not successful. Error: ' || SQLERRM;
END;

该过程本身非常快(约 1 秒),但我们使用的 IDE 建议在循环中使用 BULK COLLECT 以保持代码清洁度并确保其继续良好运行。我想这样做,但是我无法解决我应该如何选择比较日期的行,然后在使用该方法时将其与所有其他行进行比较。 BULK COLLECT 是解决这个问题的最佳方法,还是有更好的方法来进行这么多比较?

编辑

如果从表本身中选择数据会更容易,那么此视图中的数据来自这样结构的表。

SALES_DATE |  HOUR_ENDING  |  LOCATION_CODE |  VALUE
__________________________________________________________
12/27/2019        1              ABCD           40
12/27/2019        2              ABCD           50
12/27/2019        3              ABCD           51

数据必须每小时比较一次,而不是每天的总数(观察下图)。在此示例中,如果每小时比较,则总差异为 35(由于每小时都有 ABS,因为我不在乎差异是负数还是正数……只是多接近)。但是,如果将总数相加,则会返回 9 的差值。

【问题讨论】:

【参考方案1】:

--- 修订版 使用实际的源表(而不是透视视图),我们可以构建更清晰的查询。我将多个 CTE 的结构保留为 这显示了最终结果是如何构建的。我假设您已经有一个流程可以将该视图加入到请求的 sales_analysis 中。 毕竟,它由每个用户每天每个位置的单行组成。如果不是这种情况,那么您需要发布另一个问题。

由于您忽略了指定源表名,我创建了 hourly_sales 作为该源。此外,您将需要进行广泛的测试,因为您未能 提供一套完整的测试日期(从 1 天开始只有 3 个他),并且在 sales_analysis 中没有实际的预期结果。在以后的问题中,请提供 实际的表 DDL 不仅仅是一个描述。完整的测试数据 - 文本或更好的插入 - 绝不是图像。无论如何,结果如下:

create or replace procedure compare_sales_sp(
    p_compare_date       in date,
    p_location_code      in varchar2,
    p_userid             in varchar2,
    p_message            out varchar2)
is
begin
    delete from mybd.sales_analysis
     where userid = p_userid;

     insert into sales_analysis(
                               reading_date  
                              ,location_code  
                              ,userid         
                              ,proximity
                              )
        with hours_to_include as
             ( select case when trunc (sysdate) = trunc (p_compare_date)
                           then 1+to_number(to_char(sysdate, 'hh24'))
                           else 24
                      end num_hours
                 from dual
             )          
           , compare_to as
             ( select hs.* 
                 from hourly_sales hs     
                 join hours_to_include
                   on hour_ending <= num_hours
                where sales_date = p_compare_date
                  and location_code = p_location_code
             ) 
           , daily_sales as
             ( select hs.* 
                 from hourly_sales hs 
                 join hours_to_include
                   on hour_ending <= num_hours
                where location_code = p_location_code
                  and sales_date > add_months(sysdate, -120)
                  and sales_date != p_compare_date       
              )       
         select distinct   
                sales_date
              , location_code
              , p_userid 
              , prox  
           from ( select ds.sales_date
                       , ds.location_code
                       , sum(abs(ds.value - ct.value)) over( partition by ds.sales_date, ds.location_code) prox 
                    from daily_sales ds 
                    join compare_to  ct
                      on (ds.location_code = ct.location_code and 
                          ds.hour_ending   = ct.hour_ending
                         )
                 )
           where prox < 50;                                       
    commit;
    p_message := 'Sales analysis successful. Please review the results';
exception
    when others
    then
        rollback;
        p_message := 'Sales analysis was not successful. Error: ' || sqlerrm;
end compare_sales_sp;

--- 从长远来看,从逐行(又称逐行)转换为批量处理可能是有益的。但是,批量处理通常需要更多的程序逻辑。在这种情况下,我建议不要将其重构为批量过程,因为它仅采用 1/2 方式。为什么不一路走下去——完全摆脱程序代码。让 SQL 完成所有工作。您的循环主要只是从 24 小时列中计算每日金额。 SQL 很容易进行该计算。此过程 1 个 SQL 语句(不包括先前运行的删除)。

create or replace procedure mydb.compare_sales_sp (
    p_compare_date       in date,
    p_location_code      in varchar2,
    p_userid             in varchar2,
    p_message            out varchar2)
is
begin
    delete from mydb.sales_analysis
     where userid = p_userid;

     insert into mydb.sales_analysis(
                               reading_date  
                              ,location_code  
                              ,userid         
                              ,proximity
                              )
        with hours_to_include as
             ( select case when trunc (sysdate) = trunc (p_compare_date)
                           then 1+to_number(to_char(sysdate, 'hh24'))
                           else 24
                      end num_hours
                 from dual
             )          
           , compare_to as
             ( select num_hours, sales_date,location_code
                    , case when num_hours >= 1 then nvl(he1_sales,0) else 0 end
                     +case when num_hours >= 2 then nvl(he2_sales,0) else 0 end
                     +case when num_hours >= 3 then nvl(he3_sales,0) else 0 end
                     +case when num_hours >= 4 then nvl(he4_sales,0) else 0 end
                     +case when num_hours >= 5 then nvl(he5_sales,0) else 0 end 
                     as total_sales
                 from sales_by_day cross join hours_to_include
                where sales_date = p_compare_date
                  and location_code = p_location_code
             ) 
           , daily_sales as
             ( select sales_date,location_code
                    , case when num_hours >= 1 then nvl(he1_sales,0) else 0 end
                     +case when num_hours >= 2 then nvl(he2_sales,0) else 0 end
                     +case when num_hours >= 3 then nvl(he3_sales,0) else 0 end
                     +case when num_hours >= 4 then nvl(he4_sales,0) else 0 end
                     +case when num_hours >= 5 then nvl(he5_sales,0) else 0 end
                     as total_sales
                 from mydb.sales_by_day 
                cross join hours_to_include
                where location_code = p_location_code
                  and sales_date > add_months(sysdate, -120)  -- 120 months = 10 years
             ) 
        select ds.sales_date
             , ds.location_code
             , p_userid
             , abs(ds.total_sales - ct.total_sales) 
          from mydb.daily_sales ds
         cross join compare_to  ct 
         where abs(ct.total_sales -ds.total_sales) < 50;

    commit;
    p_message := 'Sales analysis successful. Please review the results';
exception
    when others
    then
        rollback;
        p_message := 'Sales analysis was not successful. Error: ' || sqlerrm;
end; 

我只使用了必要的 24 列中的 5 列,但足以让您了解需要什么。查询变化不大,但比程序代码好。这是未规范化数据并将重复组放在一行中的结果。

【讨论】:

我添加了一个带有原始表定义的编辑,因为它当前使用的数据库视图可以像这样布置数据。 db 视图主要用于在 UI 中显示网格,但我猜最初是在这里使用的,因为必须逐小时比较数据(我还附上了一张图片来演示这一点)。

以上是关于重构 Oracle 存储过程以使用 BULK COLLECT的主要内容,如果未能解决你的问题,请参考以下文章

使用Bulk Binding批量绑定的模式高效处理ORACLE大量数据

具有参数化模式名称并用于目标表中的 BULK INSERT 的动态游标

Oracle中IS TABLE OF的使用

存储过程中的 Oracle For 循环不循环

oracle 优化之批量处理bulk correct 和 forall

Oracle:创建存储过程以将另一个存储过程的结果插入表中