Оптимизация производительности PL / SQL - вычисление сумм и различий из записей изменений - PullRequest
0 голосов
/ 25 августа 2018

У меня есть таблица предметов (инвентарь) и таблица предыдущих состояний инвентаря (истории). Я хочу вычислить сумму (суммы) определенного запаса (инвентаризационный код 'A' и 'B') для указанного диапазона дат, а также его изменения (дифференциалы).

Example

Мне удалось сделать это в процедуре Oracle PL / SQL (а также реплицировать ее в SQLfiddle , абстрагируя многие вещи), но это удивительно дорогой запрос, занимающий около 20 минут. 30 дней, 60000 инвентарных записей и 120000 исторических записей.

Из одного взгляда на запрос я могу сказать, что это дорого - первый цикл - дни, второй цикл - весь инвентарь, а третий цикл - вся история для этого конкретного инвентаря.

Как я могу сделать этот запрос быстрее? Я предполагаю использовать агрегатные функции, чтобы уменьшить количество циклов, полагаясь на умные алгоритмы SQL Oracle - но я не совсем уверен, как сделать все это в одном большом запросе - или, если это вообще возможно.

Я могу добавить новые поля, которые могут улучшить эту таблицу / ускорить вычисления, но я не могу удалить какие-либо поля.

Заранее спасибо и, пожалуйста, дайте мне знать, как я могу уточнить или улучшить мой вопрос. Я новичок в 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 Ответ

0 голосов
/ 25 августа 2018

dbms_profiler показывает, что внутренний цикл («получить последнее изменение дня») был выполнен 236 раз с использованием ваших тестовых данных (10 описей и 20 историй) и занимал большую часть времени,Он запрашивает 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 - это процедура, а не запрос, а в таблицах есть столбцы, а не поля.

...