PL/SQL 性能优化 - 根据记录更改计算总和和微分
Posted
技术标签:
【中文标题】PL/SQL 性能优化 - 根据记录更改计算总和和微分【英文标题】:PL/SQL Performance Optimization - Computing Sums and Differentials from Record Changes 【发布时间】:2018-08-25 01:45:54 【问题描述】:我有一个项目表(库存)和一个库存先前状态表(历史)。我想计算指定日期范围内特定库存(inventory.itemcode 'A' 和 'B')的数量(总和),以及它的变化(差异)。
我能够在 Oracle PL/SQL 过程中执行此操作(以及在 SQLfiddle 中复制它,抽象出许多东西),但这是一个非常昂贵的查询,30 天大约需要 20 分钟,60000库存记录,120000条历史记录。
从查询的角度来看,我可以看出它很昂贵 - 第一个循环是天数,第二个循环是整个库存,第三个循环是该特定库存的整个历史记录。
我怎样才能使这个查询更快?我的猜测是使用聚合函数来减少循环量,依赖于 Oracle 的智能 SQL 算法——但我不太确定如何将所有这些变成一个大查询——或者这是否可能。
我可以添加新的字段以使该表更好/使计算更快,但我无法删除任何字段。
提前致谢,请告诉我如何澄清或改进我的问题。我是 PL/SQL 的新手,所以如果我犯了任何新手错误,请告诉我。
代码:
procedure do
is
start_date number;
end_date number;
start_date_date date;
end_date_date date;
currentDate varchar2(10);
-- assoc array of itemcode[itemid]
type code_id_table_type is table of varchar2(10) index by PLS_INTEGER;
code_id_table code_id_table_type;
elem varchar2(10) default ' ';
-- positive
dateA number default 0;
dateB number default 0;
-- negative
dateAn number default 0;
dateBn number default 0;
-- running tally
summA number default 0;
summB number default 0;
begin
log('DAY | sum(A B) | diff+(A B) | diff-(A B) ' || CHR(10));
-- passed in START date and END date. these literals will be used below.
start_date_date := to_date('15-JUN-18', 'DD-MON-YY');
end_date_date := to_date('25-JUN-18', 'DD-MON-YY');
start_date := to_number(to_char(start_date_date, 'j'));
end_date := to_number(to_char(end_date_date, 'j'));
-- compute previous items
SELECT COUNT(*) INTO summA FROM
(
SELECT DISTINCT t1.itemid, t1.itemcode, t1.changedate
FROM histories t1,
(SELECT itemid, max(changedate) as changedate
FROM histories
WHERE changedate < start_date_date
GROUP BY itemid) t2
WHERE t1.itemid = t2.itemid
AND t1.changedate = t2.changedate
AND t1.itemcode = 'A'
);
SELECT COUNT(*) INTO summB FROM
(
SELECT DISTINCT t1.itemid, t1.itemcode, t1.changedate
FROM histories t1,
(SELECT itemid, max(changedate) as changedate
FROM histories
WHERE changedate < start_date_date
GROUP BY itemid) t2
WHERE t1.itemid = t2.itemid
AND t1.changedate = t2.changedate
AND t1.itemcode = 'B'
);
-- compute a itemcode(itemid) array for all inventory as of given date
for outerrec in (
SELECT itemid, itemcode
FROM inventory
)
loop
for innerrec in (
SELECT itemid, itemcode, changedate
FROM histories
WHERE itemid = outerrec.itemid
AND changedate < start_date_date
AND ROWNUM = 1
ORDER BY changedate DESC
)
loop
code_id_table(innerrec.itemid) := innerrec.itemcode;
end loop;
end loop;
-- compute differentials for every day.
for daterec in start_date..end_date
loop
-- date iterator
currentdate := To_char(To_date(daterec, 'j'), 'DD-MON-YY');
-- reset counts
dateA := 0;
dateB := 0;
dateAn := 0;
dateBn := 0;
for outerrec in (
SELECT itemid, itemcode
FROM inventory
)
loop
-- get the last change of the day
for innerrec in (
SELECT itemid, itemcode, changedate
FROM histories
WHERE itemid = outerrec.itemid
AND changedate >= to_date(currentdate)
AND changedate < to_date(currentdate)+1
AND ROWNUM = 1
ORDER BY changedate DESC
)
loop
-- check existence in code table
if (code_id_table.exists(innerrec.itemid)) then
-- check if the code was lost that day
if (code_id_table(innerrec.itemid) = 'A') then
dateAn := dateAn + 1;
elsif (code_id_table(innerrec.itemid) = 'B') then
dateBn := dateBn + 1;
end if;
-- check if the code was gained that day
if (innerrec.itemcode = 'A') then
dateA := dateA + 1;
elsif (innerrec.itemcode = 'B') then
dateB := dateB + 1;
end if;
else
-- new item, code is gained
if (innerrec.itemcode = 'A') then
dateA := dateA + 1;
elsif (innerrec.itemcode = 'B') then
dateB := dateB + 1;
end if;
end if;
-- update code table
code_id_table(innerrec.itemid) := innerrec.itemcode;
end loop;
end loop;
-- compute sums
summA := summA + (nvl(dateA, 0) + (0 - nvl(dateAn, 0)));
summB := summB + (nvl(dateB, 0) + (0 - nvl(dateBn, 0)));
-- output results
log(To_char(To_date(currentdate), 'YYYY-MM-DD') || ' (' || summA || ' ' || summB || ') (+' || dateA || ' +' || dateB || ') (-' || dateAn || ' -' || dateBn || ') ' || CHR(10));
end loop;
SQLfiddle
【问题讨论】:
【参考方案1】:dbms_profiler
揭示了内部循环 ("get the last change of the day") 使用您的测试数据(10 个库存和 20 个历史记录)执行了 236 次,并占用了大部分时间。它一次查询histories
一行where itemid = outerrec.itemid
,因此一个快速解决方法可能是在histories(items, changedate, itemcode)
上放置一个索引。这并不能真正替代高级重组,因为所有这些循环本质上都是资源密集型的,但我不确定它想要做什么。您想要的结果的一些示例会有所帮助。
这个查询可能不符合你的要求:
SELECT itemid, itemcode, changedate
FROM histories
WHERE itemid = outerrec.itemid
AND changedate >= to_date(currentdate)
AND changedate < to_date(currentdate)+1
AND ROWNUM = 1
ORDER BY changedate DESC
rownum
是在订购之前生成的,所以你得到一个任意行然后订购它。假设是最新版本的 Oracle,那应该是
order by changedate desc
fetch first row only
对于旧版本,您可以使用解析 row_number()
生成排序键,然后将整个内容嵌套在内联视图中,因为您不能直接在 order by
子句中使用解析函数。
另外,to_date(currentdate)
可能仅适用于您当前的桌面设置,但您应该真正使用显式转换格式或(甚至更好)将current date
声明为date
并避免大量类型转换。
顺便说一句,pkg_test.do
是一个过程,而不是查询,并且表有列,而不是字段。
【讨论】:
感谢您的回答@william。我刚刚学会了如何使用dbms_profiler
,它让我对我的查询有了很大的了解。我相信innerRec
正在检索正确的记录,并且查询正在检索我想要的结果 - 我正在运行 11g。我会确保应用这些更改并在星期一试一试。当前索引(在生产系统上)位于histories(itemid, changedate)
- 将histories(itemcode)
添加到索引中会显着提高速度吗?
嗨@William - 结果是索引使查询变慢,Oracle 选择不使用索引就证明了这一点......我相信这是因为在 prod 系统上,我们使用四个不同的键来映射inventory
和 histories
在一起。
好的。我将我的 cmets 建立在没有索引的 SQL Fiddle 上。也可能上面的查询是实际代码的简化版本。我的想法是,如果您基于itemid
和changedate
获取itemid
、itemcode
和changedate
,那么这三列上的索引(最后是itemcode
)将意味着查询将不会根本不需要访问桌子。但是,如果实际情况不同,则需要调整该查询的版本(即 DBMS_PROFILER 告诉您的查询被执行了无数次)。
非常感谢 - 我想我可以从这里弄清楚!以上是关于PL/SQL 性能优化 - 根据记录更改计算总和和微分的主要内容,如果未能解决你的问题,请参考以下文章