3

(これはすべて Oracle 10g です):

CREATE OR REPLACE FUNCTION bar(...)
IS
    v_first_type VARCHAR2(100) ;
    v_second_type VARCHAR2(100);

    CURSOR cur IS SELECT a,b FROM source_table ;
    v_a int;
    v_b char;
BEGIN
    OPEN cur;
    <<l_next>> --10G doesn't have the continue statement.
    LOOP
        FETCH cur INTO v_a, v_b ;
        EXIT WHEN cur%NOTFOUND ;

        --Ignore Record Case: ignore the record entirely
        IF a == -1 THEN
            -- do something
            GOTO l_next ; --10g doesn't have the continue statement.
        ELSE
            -- do something else
            v_first := 'SUCCESS' ;
        END IF;

        -- Transform Case:
        IF b == 'z' THEN
            -- do something
            v_second := 'something';
        ELSE
            -- do something
            v_second := 'something else';
        END IF;


        INSERT INTO report_table VALUES (v_first, v_second);
    END LOOP;
    CLOSE cur;
EXCEPTION 
    ...
END;

私は大学を出て初めての仕事に就いています。上記の一般的なフレームワークのように見えるいくつかのレガシーコードを調べています(ただし、数百行の長さであり、はるかに複雑な処理を使用します(セットベースのソリューションは不可能です))。

1 つのテーブルからカーソルに多数の行をプルし、カーソルをループして、カーソルを変換し、結果をレポート テーブルに挿入します。カーソルはすべてのレコードを挿入するわけではありません。レコードに問題がある場合、または何らかの理由で気に入らない場合は、レコードを挿入せずにスキップします (GOTO ステートメントを参照)。

問題 1: 挿入は、ループの外側で最後に FORALL を実行するのではなく、ループの内側で 1 つずつ発生しています。

問題 2: カーソルが BULK COLLECT を使用していません。

これに加えて、BULK COLLECT を使用しないカーソルを持つストアド プロシージャがあり、カーソル内のレコードをループしながらこの関数を発行します。ループされた各レコードの最後に 1 つのコミットが発行されます。ここで書いている関数にはコミットはありません。

コードを次のように書き直したいと思います。

CREATE OR REPLACE FUNCTION bar(...)
IS
    CURSOR cur IS SELECT a,b FROM source_table ;

    TYPE t_source IS TABLE OF cur%ROWTYPE INDEX BY PLS_INTEGER;
    TYPE t_report IS TABLE OF destination_table%ROWTYPE INDEX BY PLS_INTEGER;
    v_sources t_source;
    v_reports t_report
    v_report_inx INT := 0; -- To Prevent Sparse Collection
BEGIN
    OPEN cur;
    <<l_next>> --10G doesn't have the continue statement.
    LOOP
        FETCH cur BULK COLLECT INTO v_sources LIMIT 100 ;
        EXIT WHEN v_sources.count = 0 ;

        FOR i IN 1 .. v_sources LOOP
            --Ignore Record Case: ignore the record entirely
            IF v_sources(i).a == -1 THEN
                -- do something
                GOTO l_next ; --10g doesn't have the continue statement.
            ELSE
                -- do something else
                v_reports(v_report_inx).first := 'SUCCESS' ;
            END IF;

            -- Transform Case:
            IF v_sources(i).b == 'z' THEN
                -- do something
                v_reports(v_report_inx).second := 'something';
            ELSE
                -- do something
                v_reports(v_report_inx).second := 'something else';
            END IF;

            v_report_inx := v_report_inx + 1;
        END LOOP;


    END LOOP;

    FORALL i in 1 .. v_reports.count
            INSERT INTO report_table (first, second) VALUES (v_reports(i).first, v_reports(i).v_second);

    CLOSE cur;
EXCEPTION 
    ...
END;

重要な変更点は、1) 連想配列への BULK COLLECT の使用、および 2) 別の連想配列からの FORALL の使用です。

2 つの質問があります。

1) 最初のスニペットで提供したフレームワークに基づいて、私の変更はそれを行うための最も優れた方法ですか? 別の方法でやりますか?

2) 誰かが BULK COLLECT と FORALL を使用しない理由を私が考えていない理由はありますか? おそらく、レガシー コードではまだ気付いていない複雑な処理でしょうか。このコードはもともと 2002 年に作成されました (したがって、8i または 9i であると推測されます) が、その後更新されています。9i には一括バインディングがありました。8i にも一括バインディングがありました。どちらも連想配列を持っていました。ですから、一括バインディングを使用していなかったのには理由があるに違いないと思います。

4

2 に答える 2

2

に移行するという一般的な考え方に問題はありませんbulk collect。一括操作は、コンテキスト スイッチとデータベース ラウンド トリップの数を最小限に抑えるだけです。

コードに問題がある一般的なものは1つだけです。LIMIT句は、一括操作によるメモリの過剰使用を防ぐため、一括収集で使用するのが適切な決定です。しかし、v_reports制御不能に成長します。したがって、一括挿入をループ内に移動し、v_reportsその後クリアします。

変更されたコードにはいくつかの不正確さがあります。/**/以下のコードフラグメントを確認してください。スタイル のコメントは私のものです。

CREATE OR REPLACE FUNCTION bar(...)
IS
    CURSOR cur IS SELECT a,b FROM source_table ;

    TYPE t_source IS TABLE OF cur%ROWTYPE INDEX BY PLS_INTEGER;
    TYPE t_report IS TABLE OF destination_table%ROWTYPE INDEX BY PLS_INTEGER;
    v_sources t_source;
    v_reports t_report

    /* 1. correct type is same as type of index
       2. There are nothing wrong with sparse collections, but a separate 
           counter which incremented continuously needed for t_report.
    */
    v_report_inx PLS_INTEGER := 0; -- To Prevent Sparse Collection

BEGIN
    OPEN cur;
    <<l_next>> --10G doesn't have the continue statement.
    LOOP
        FETCH cur BULK COLLECT INTO v_sources LIMIT 100 ;

        /* On last step v_sources.count < 100, not exactly 0.
           Also if there are no elements then no processing done, 
           so check at the end of loop.
        EXIT WHEN v_sources.count = 0;
        */

        /* correct way is to loop from 1 to count
          (.last and .first not usable because both is null for empty array)
        */
        FOR i IN 1  .. v_sources.count LOOP

            v_report_inx := v_report_inx + 1;

            --Ignore Record Case: ignore the record entirely
            IF v_sources(i).a = -1 THEN
                -- do something
                GOTO l_next ; --10g doesn't have the continue statement.
            END IF;

            /* No need for ELSE here, just execution continues */
             -- do something else
             v_reports(v_report_inx).first := 'SUCCESS' ;


            -- Transform Case:
            IF v_sources(i).b = 'z' THEN
                -- do something
                v_reports(v_report_inx).second := 'something';
            ELSE
                -- do something
                v_reports(v_report_inx).second := 'something else';
            END IF;

        END LOOP;


        /* Use "indicies of" construct to deal with sparsed collections */
        FORALL i in indices of v_reports
              /* t_report already declared with %ROWTYPE 
                 so just insert entire row, it works faster */
              INSERT INTO report_table VALUES v_reports(i);

        /* Cleanup after insert */
        v_reports.delete;

        /* If number of selected records less than LIMIT then last row reached. */
        EXIT WHEN v_sources.count < 100;

    END LOOP;


    CLOSE cur;
EXCEPTION
    ...
END;

アップデート

@jonearles に感謝します。彼は、PL/SQL でカーソルを処理するためのさまざまなアプローチのパフォーマンスをテストするように勧めてくれました。

以下は、3 000 000 レコードを使用したテストの結果です。単純な明示的カーソルから一括収集アプローチへの移行により、実際のパフォーマンスが向上することは明らかです。
同時に、一括収集オプションと適切に選択された LIMIT を備えた明示的カーソルは、常に暗黙的カーソルよりも優れていますが、それらの違いは許容範囲内にあります。

Variant name           | Time (sec)
-------------------------------------
bulk_cursor_limit_500  |  1.26
bulk_cursor_limit_100  |  1.52
bulk_unlimited         |  1.75
implicit_cursor        |  1.83
plain_cursor           | 27.20

以下はテスト用のコードです(限定された SQLFiddle の例はこちら

スキームのセットアップ

drop table t
/
drop table log_run
/
create table t(a number, b number)
/
insert into t select level, level from dual connect by level <= 3000000
/

create table log_run(id varchar2(30), seconds number);
/

delete log_run
/

1 回のテスト実行

declare
  cursor test_cur is
    select a, b from t;

  test_rec test_cur%rowtype;
  counter    number;

  vStart timestamp;
  vEnd timestamp;
  vTimeFormat varchar2(30) := 'SSSSS.FF9';
begin

  vStart := systimestamp;

  open test_cur;
  loop
    fetch test_cur into test_rec;
    exit when test_cur%notfound;
    counter := counter + 1;
  end loop;
  close test_cur;

  vEnd := systimestamp;
  insert into log_run(id, seconds) 
    values('plain_cursor', 
             to_number(to_char(vEnd,vTimeFormat))
             -
             to_number(to_char(vStart,vTimeFormat)) 
          )
  ;

end;
/

--Implicit cursor
--0.2 seconds
declare
  test_rec   t%rowtype;
  counter    number;

  vStart timestamp;
  vEnd timestamp;
  vTimeFormat varchar2(30) := 'SSSSS.FF9';
begin

  vStart := systimestamp;

  for c_test_rec in (select a, b from t) loop
    test_rec.a := c_test_rec.a;
    test_rec.b := c_test_rec.b;
    counter := counter + 1;
  end loop;

  vEnd := systimestamp;
  insert into log_run(id, seconds) 
    values('implicit_cursor', 
             to_number(to_char(vEnd,vTimeFormat))
             -
             to_number(to_char(vStart,vTimeFormat)) 
          )
  ;

end;
/

declare
  cursor test_cur is
    select a, b from t;

  type t_test_table is table of t%rowtype;

  test_tab   t_test_table;
  counter    number;

  vStart timestamp;
  vEnd timestamp;
  vTimeFormat varchar2(30) := 'SSSSS.FF9';
begin

  vStart := systimestamp;

  open test_cur;
  loop
    fetch test_cur bulk collect into test_tab limit 100;
    for i in 1 .. test_tab.count loop
      counter := counter + 1;
    end loop;

    exit when test_tab.count < 100;
  end loop;

  close test_cur;

  vEnd := systimestamp;
  insert into log_run(id, seconds) 
    values('bulk_cursor_limit_100', 
             to_number(to_char(vEnd,vTimeFormat))
             -
             to_number(to_char(vStart,vTimeFormat)) 
          )
  ;

end;
/


declare
  cursor test_cur is
    select a, b from t;

  type t_test_table is table of t%rowtype;

  test_tab   t_test_table;
  counter    number;

  vStart timestamp;
  vEnd timestamp;
  vTimeFormat varchar2(30) := 'SSSSS.FF9';
begin

  vStart := systimestamp;

  open test_cur;
  loop
    fetch test_cur bulk collect into test_tab limit 500;
    for i in 1 .. test_tab.count loop
      counter := counter + 1;
    end loop;

    exit when test_tab.count < 500;
  end loop;

  close test_cur;

  vEnd := systimestamp;
  insert into log_run(id, seconds) 
    values('bulk_cursor_limit_500', 
             to_number(to_char(vEnd,vTimeFormat))
             -
             to_number(to_char(vStart,vTimeFormat)) 
          )
  ;

end;
/

declare

  type t_test_table is table of t%rowtype;

  test_tab   t_test_table;
  counter    number;

  vStart timestamp;
  vEnd timestamp;
  vTimeFormat varchar2(30) := 'SSSSS.FF9';
begin

  vStart := systimestamp;

  select * bulk collect into test_tab from t;

  for i in 1 .. test_tab.count loop
    counter := counter + 1;
  end loop;

  vEnd := systimestamp;
  insert into log_run(id, seconds) 
    values('bulk_unlimited', 
             to_number(to_char(vEnd,vTimeFormat))
             -
             to_number(to_char(vStart,vTimeFormat)) 
          )
  ;

end;
/

平均結果を選択

select * from ( 
  select lr.id, trunc(avg(seconds),2) seconds  
  from log_run lr group by lr.id) 
  order by seconds
)
于 2013-08-10T09:11:09.483 に答える
1

GOTO が使用されないようにこれを書き直します (私は、本質的には再構築されていない古い構造化プログラマーに過ぎないと思います :-)。また、明示的なカーソルを取り除き、カーソルの FOR ループを使用します。これは、10g 以降ではしばしば舞台裏で一括バインドされます。試す:

CREATE OR REPLACE FUNCTION bar(...)
IS
    v_first_type VARCHAR2(100) ;
    v_second_type VARCHAR2(100);
BEGIN
    <<OUTER_LOOP>>
    FOR aRow In (SELECT A, B FROM SOURCE_TABLE)
    LOOP

      <<INNER_LOOP>>
      LOOP  -- This loop is used to allow us to skip the later INSERT, and
            -- will only be passed through once for each row returned by
            -- the FOR loop.
        --Ignore Record Case: ignore the record entirely
        IF aRow.A == -1 THEN
            -- do something
            EXIT INNER_LOOP;  -- rather than GOTO
        ELSE
            -- do something else
            v_first := 'SUCCESS' ;
        END IF;

        -- Transform Case:
        IF aRow.B == 'z' THEN
            -- do something
            v_second := 'something';
        ELSE
            -- do something
            v_second := 'something else';
        END IF;


        INSERT INTO report_table VALUES (v_first, v_second);

        EXIT INNER_LOOP;  -- the "loop" is used to allow the INSERT to be
                          -- skipped and thus we don't ever want to go back
                          -- to the top
      END LOOP;  -- INNER_LOOP
    END LOOP;  -- OUTER_LOOP
EXCEPTION 
    ...
END;

ループの明示的な制御を可能にしながら、トップダウン フローを維持するために、明示的な出口で内側のループを使用していることに注意してください。

また、このコードを Oracle プロファイラーで実行して、どのコード行が最も時間を消費しているかを理解することをお勧めします。ボトルネックがどこにあるかを推測してコードを最適化しようとするのは、時間の無駄です。プロファイリングを行うまでは、あなたは推測しているにすぎません。:-) コードは最悪の場所で時間を費やします...

共有してお楽しみください。

于 2013-08-10T11:21:30.670 に答える