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 系统上,我们使用四个不同的键来映射inventoryhistories 在一起。 好的。我将我的 cmets 建立在没有索引的 SQL Fiddle 上。也可能上面的查询是实际代码的简化版本。我的想法是,如果您基于itemidchangedate 获取itemiditemcodechangedate,那么这三列上的索引(最后是itemcode)将意味着查询将不会根本不需要访问桌子。但是,如果实际情况不同,则需要调整该查询的版本(即 DBMS_PROFILER 告诉您的查询被执行了无数次)。 非常感谢 - 我想我可以从这里弄清楚!

以上是关于PL/SQL 性能优化 - 根据记录更改计算总和和微分的主要内容,如果未能解决你的问题,请参考以下文章

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

如何确保在编译 PL/SQL 程序时启用了优化?

百倍性能的PL/SQL优化案例(r11笔记第13天)

PL/SQL 每小时总和

Oracle PL/SQL:如何根据同一记录的 ID 字段返回字符串值?

如何优化使用游标的 PL/SQL 代码