重构 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 的动态游标