2017-12-06 6 views
1

巨大なテーブル(> 50.000.000)の更新で驚くほど速く動作する次のPL/SQL BULK-COLLECTを一緒に試しました。唯一の問題は、残りの<テーブルあたり5000行の更新を実行しないことです。 5000はFETCH命令の所定の制限です。BULK COLLECTの奇妙な動作

DECLARE 
-- source table cursor (only columns to be updated) 
    CURSOR base_table_cur IS 
    select a.rowid, TARGET_COLUMN from TARGET_TABLE a 
         where TARGET_COLUMN is null; 

    TYPE base_type IS 
     TABLE OF base_table_cur%rowtype INDEX BY PLS_INTEGER; 
    base_tab base_type;  

-- new data 
    CURSOR new_data_cur IS 
     select a.rowid, 
     coalesce(b.SOURCE_COLUMN, 'FILL_VALUE'||a.JOIN_COLUMN) TARGET_COLUMN from TARGET_TABLE a 
     left outer join SOURCE_TABLE b 
     on a.JOIN_COLUMN=b.JOIN_COLUMN 
     where a.TARGET_COLUMN is null; 

    TYPE new_data_type IS TABLE OF new_data_cur%rowtype INDEX BY PLS_INTEGER; 
    new_data_tab  new_data_type; 
    TYPE row_id_type IS TABLE OF ROWID INDEX BY PLS_INTEGER; 
    row_id_tab   row_id_type; 
    TYPE rt_update_cols IS RECORD (
     TARGET_COLUMN TARGET_TABLE.TARGET_COLUMN%TYPE 
    ); 
    TYPE update_cols_type IS 
     TABLE OF rt_update_cols INDEX BY PLS_INTEGER; 
    update_cols_tab update_cols_type; 
    dml_errors EXCEPTION; 
    PRAGMA exception_init (dml_errors,-24381); 

BEGIN 
    OPEN base_table_cur; 
    OPEN new_data_cur; 
    LOOP 
     FETCH base_table_cur BULK COLLECT INTO base_tab LIMIT 5000; 
     IF base_table_cur%notfound THEN 
      DBMS_OUTPUT.PUT_LINE('Nothing to update. Exiting.'); 
      EXIT; 
     END IF; 
     FETCH new_data_cur BULK COLLECT INTO new_data_tab LIMIT 5000; 
     FOR i IN base_tab.first..base_tab.last LOOP 
      row_id_tab(i) := new_data_tab(i).rowid; 
      update_cols_tab(i).TARGET_COLUMN := new_data_tab(i).TARGET_COLUMN; 
     END LOOP; 

     FORALL i IN base_tab.first..base_tab.last SAVE EXCEPTIONS 
      UPDATE (SELECT TARGET_COLUMN FROM TARGET_TABLE) 
      SET row = update_cols_tab(i) 
      WHERE ROWID = row_id_tab(i); 

     COMMIT; 
     EXIT WHEN base_tab.count < 5000; -- changing to 1 didn't help! 
    END LOOP; 

    COMMIT; 
    CLOSE base_table_cur; 
    CLOSE new_data_cur; 
EXCEPTION 
    WHEN dml_errors THEN 
     FOR i IN 1..SQL%bulk_exceptions.count LOOP 
      dbms_output.put_line('Some error occured'); 
     END LOOP; 
END; 

ここで私の間違いはありますか?それは私には正しいように見えます。

+1

問題には関係ありませんが、ループの内側にコミットするのは悪い考えです。問題がある場合は、開始するために再起動するのがずっと難しくなります。 –

+0

私が理解する限り、それはパフォーマンスがどこから来るのか正確です。 – royskatt

+1

番号[トランザクションの最後にコミット](https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:74669804982988)。そして、なぜ[頻繁なコミットが悪い]という理由でTomから(https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:4951966319022)。バルク・コレクトの使用「PL/SQLとSQL間の通信のパフォーマンス・オーバーヘッドを最小限に抑える」(https://docs.oracle.com/database/122/LNPLS/plsql-optimization-and-tuning.htm#GUID-61D1B533) -DBB9-4150-91F9-0A4C9428391E)。 –

答えて

4

問題は、この行である:見つかったレコードの数がLIMIT値より小さいとき

IF base_table_cur%notfound THEN 

カーソルが%NOTFOUNDを満たしています。したがって、最後のフェッチが正確に5000でない場合、それらのレコードは処理されません。

BULK COLLECT ... LIMITを初めてお使いの方にはよくある問題です。ソリューションは

EXIT when base_tab.count() = 0; 

に終了条件を変更することです「私はそれがある場合base_table_curが空で、終了していないことを、確認する必要があります。それが空の場合I'lはエラーが発生します」

new_data_curカーソルはbase_table_curカーソルで選択されたテーブルを含みます。だから私はあなたが2つのループが必要とは思わない。最初のカーソルが何かを返すかどうかを確認するための簡単なテストが必要です。次に、2番目のカーソルの周りをループします。

私はあなたのロジックが完全にはっきりしていないので、私はあなたが必要と思う構造の種類をできるだけ変更していません。しかし、UPDATEステートメントはちょっと奇妙なので、まだ問題にぶつかっているかもしれません。

OPEN base_table_cur; 
FETCH base_table_cur BULK COLLECT INTO base_tab LIMIT 1; 
if base_table_tab.count = 0 then 
    DBMS_OUTPUT.PUT_LINE('Nothing to update. Exiting.'); 
else 
    OPEN new_data_cur; 
    LOOP 
     FETCH new_data_cur BULK COLLECT INTO new_data_tab LIMIT 5000; 
     exit when new_data_tab.count() = 0; 

     FOR i IN base_tab.first..base_tab.last LOOP 
      row_id_tab(i) := new_data_tab(i).rowid; 
      update_cols_tab(i).TARGET_COLUMN := new_data_tab(i).TARGET_COLUMN; 
     END LOOP; 

     FORALL i IN base_tab.first..base_tab.last SAVE EXCEPTIONS 
      UPDATE (SELECT TARGET_COLUMN FROM TARGET_TABLE) 
      SET row = update_cols_tab(i) 
      WHERE ROWID = row_id_tab(i); 

    END LOOP; 
    CLOSE new_data_cur; 
end if; 
COMMIT; 
CLOSE base_table_cur; 
+0

私は、base_table_curが空でないことを確認し、存在する場合は終了する必要があります。それが空であればエラーになります。 – royskatt

+2

おそらく、出口条件が 'notfound'チェックを置き換え、' fetch'の直後にあることを明らかにする価値があります。ループの終わりにすでに「終了時」が存在しているだけなので、これは冗長なので、変更する必要のある既存の状態であるかのように見える可能性があります。 –

+0

@APC:ループからコミットを移動したことに気づいた。私はループ内でコミットするのは良い習慣ではないことを知っていますが、最大500.000.000行のテーブルを更新する必要があるとき、私はどうにかしてこのDevide&Conquo​​rのアプローチが更新を高速化するという感覚を持っています。 このケースでは、INSERTを使用した任意のバリエーションが推奨されるはずですが、時にはクライアントに奇妙な状況があり、そうすることを妨げることがあります。つまり、最悪のケースでは、500.000.000を一度に5000回コミットする(おそらく75.000に上昇するはずです)。後者は私の平凡な意見に明確な利点があります。 – royskatt