使用FORALL + BULK COLLECT來批量插入優化大事務

開發人員找到我說,報表系統中有一個存儲過程最近總是報錯ORA-02050:transaction … . rolled back,some remote DBs may be in-doubt。
根據錯誤信息可知,可能是由於遠程數據庫處理失敗,導致事務失敗而回滾。原因可能是網絡不穩定,也可能是remote節點的連接超時,也有可能被kill了等。
系統室負責人說了,公司的網絡絕對沒問題,報錯肯定是你們程序寫的有問題,所以只能從代碼上找原因了。
諮詢了開發人員大概的情況,說是這個存儲過程只操作了一張表,單純的insert數據,每次的數據量基本固定在4500萬行左右。我粗略看了下存儲過程,
此存儲過程的核心操作其實就是一個DML的大事務,估計一個小時執行不完。同時我向開發人員驗證了,他們說不報錯的時候都是在一個小時內執行完畢的,
執行時間超過一個小時就會報錯ORA-02050。至此,可以推斷出,不是網絡問題,是程序太耗時,運行時間超過了remote端設置的超時時間(即1小時)。
因此,我向開發人員提議,改寫存儲過程,把大事務粒度化,從而提高執行時間,提高存儲過程的執行性能。

源存儲過程源碼如下:

procedure  SP_RF_CB_ACCT(kjrq in varchar2,e_errcode out char) is


 --**定義存儲過程使用的相關變量
 ...省略業務相關的變量...
 
  --**刪除目標表的數據
 EXECUTE IMMEDIATE 'truncate table CB_ACCT';
 

  --**把表的數據加載到目標表中

 INSERT /*+ APPEND */ INTO  CB_ACCT NOLOGGING
(
  ACCT_NO        ,
  CURRENCY       ,
  GL_ACCT_NO     ,
  BRANCH_NO      ,
  CUSTOMER_NO    ,
  SYS_ID         ,
  ACCT_TYPE      ,
  OPEN_DATE      ,
  LST_MNT_DT     ,
  STATUS         ,
  CUSTOMER_NAME  ,
  CURR_VAL       ,
  CURR_VAL_TOCNY ,
  SOUR_CODE      ,
  CUSTOMER_TYPE
)
select
  trim(ACCT_NO)    ,
  trim(CURRENCY)   ,
  trim(GL_ACCT_NO) ,
  trim(BRANCH_NO)  ,
  trim(CUSTOMER_NO),
  trim(SYS_ID)     ,
  trim(ACCT_TYPE)  ,
  OPEN_DATE        ,
  LST_MNT_DT       ,
  trim(STATUS)     ,
  utl_raw.cast_to_varchar2(CUSTOMER_NAME) CUSTOMER_NAME  ,
  CURR_VAL         ,
  CURR_VAL_TOCNY   ,
  trim(SOUR_CODE)  ,
  trim(CUSTOMER_TYPE)
from v_CB_ACCT@REPORT_RIFM;

 --**記錄成功執行的記錄數
  v_success := sql%rowcount;
  commit;


exception
  --**總程序異常處理部分
  when others then
    begin
      ...省略一些業務處理邏輯....
   end;
end  SP_RF_CB_ACCT;

分析以上的存儲過程代碼:v_CB_ACCT@REPORT_RIFM是通過DBLINK從remote端獲取4500行數據,一次性插入到本地的目標中,明顯是個大事務。

使用嵌套表,改寫後的存儲過程如下:

procedure  SP_RF_CB_ACCT(kjrq in varchar2,e_errcode out char) is

 --**定義存儲過程使用的相關變量
  ......此處省略......
  
  --定義CUESOR,此處會獲取到4500萬行左右的記錄
  CURSOR cb_cur IS 
        select  trim(ACCT_NO),
                trim(CURRENCY),
                trim(GL_ACCT_NO),
                trim(BRANCH_NO),
                trim(CUSTOMER_NO),
                trim(SYS_ID),
                trim(ACCT_TYPE),
                OPEN_DATE,
                LST_MNT_DT,
                trim(STATUS),
                utl_raw.cast_to_varchar2(CUSTOMER_NAME) CUSTOMER_NAME,
                CURR_VAL,
                CURR_VAL_TOCNY ,
                trim(SOUR_CODE),
                trim(CUSTOMER_TYPE)
          from v_CB_ACCT@REPORT_RIFM;
            
  --定義與表CB_ACCT中每個列的類型對應的類型
  TYPE type_ACCT_NO     IS TABLE OF  CB_ACCT.ACCT_NO%TYPE;
  TYPE type_CURRENCY         IS TABLE OF  CB_ACCT.CURRENCY%TYPE;
  TYPE type_GL_ACCT_NO       IS TABLE OF  CB_ACCT.GL_ACCT_NO%TYPE;
  TYPE type_BRANCH_NO        IS TABLE OF  CB_ACCT.BRANCH_NO%TYPE;
  TYPE type_CUSTOMER_NO      IS TABLE OF  CB_ACCT.CUSTOMER_NO%TYPE;
  TYPE type_SYS_ID           IS TABLE OF  CB_ACCT.SYS_ID%TYPE;
  TYPE type_ACCT_TYPE        IS TABLE OF  CB_ACCT.ACCT_TYPE%TYPE;
  TYPE type_OPEN_DATE        IS TABLE OF  CB_ACCT.OPEN_DATE%TYPE;
  TYPE type_LST_MNT_DT       IS TABLE OF  CB_ACCT.LST_MNT_DT%TYPE;
  TYPE type_STATUS           IS TABLE OF  CB_ACCT.STATUS%TYPE;
  TYPE type_CUSTOMER_NAME    IS TABLE OF  CB_ACCT.CUSTOMER_NAME%TYPE;
  TYPE type_CURR_VAL         IS TABLE OF  CB_ACCT.CURR_VAL%TYPE;
  TYPE type_CURR_VAL_TOCNY   IS TABLE OF  CB_ACCT.CURR_VAL_TOCNY%TYPE;
  TYPE type_SOUR_CODE        IS TABLE OF  CB_ACCT.SOUR_CODE%TYPE;
  TYPE type_CUSTOMER_TYPE    IS TABLE OF  CB_ACCT.CUSTOMER_TYPE%TYPE;
  
  --定義嵌套表
  acct_no_tab         type_ACCT_NO;
  currency_tab        type_CURRENCY;
  gl_acct_no_tab      type_GL_ACCT_NO;
  branch_no_tab       type_BRANCH_NO;
  customer_no_tab     type_CUSTOMER_NO;
  sys_id_tab          type_SYS_ID;
  acct_type_tab       type_ACCT_TYPE;
  open_date_tab       type_OPEN_DATE;
  lst_mnt_dt_tab      type_LST_MNT_DT;
  status_tab          type_STATUS;
  customer_name_tab   type_CUSTOMER_NAME;
  curr_val_tab        type_CURR_VAL;
  curr_val_tocny_tab  type_CURR_VAL_TOCNY;
  sour_code_tab       type_SOUR_CODE;
  customer_type_tab   type_CUSTOMER_TYPE;
  
  --定義分批插入時,每次插入的最大數據條目,5百萬行
  v_limit pls_integer := 5000000;

begin

  --**刪除目標表的數據
 EXECUTE IMMEDIATE 'truncate table CB_ACCT';

  --**把表的數據加載到目標表中
  OPEN cb_cur;
    LOOP
        FETCH cb_cur BULK COLLECT INTO acct_no_tab,
                                      currency_tab,
                                      gl_acct_no_tab,
                                      branch_no_tab,
                                      customer_no_tab,
                                      sys_id_tab,
                                      acct_type_tab,
                                      open_date_tab,
                                      lst_mnt_dt_tab,
                                      status_tab,
                                      customer_name_tab,
                                      curr_val_tab,
                                      curr_val_tocny_tab,
                                      sour_code_tab,
                                      customer_type_tab LIMIT v_limit;
          EXIT WHEN acct_no_tab.COUNT=0;
          
         FORALL i in 1..acct_no_tab.COUNT
          INSERT /*+ APPEND */ INTO  CB_ACCT NOLOGGING
              (ACCT_NO,CURRENCY,GL_ACCT_NO,BRANCH_NO,CUSTOMER_NO,SYS_ID, ACCT_TYPE,
                OPEN_DATE,LST_MNT_DT,STATUS,CUSTOMER_NAME,CURR_VAL,CURR_VAL_TOCNY,SOUR_CODE,CUSTOMER_TYPE
              ) values (
                acct_no_tab(i),
                currency_tab(i),
                gl_acct_no_tab(i),
                branch_no_tab(i),
                customer_no_tab(i),
                sys_id_tab(i),
                acct_type_tab(i),
                open_date_tab(i),
                lst_mnt_dt_tab(i),
                status_tab(i),
                customer_name_tab(i),
                curr_val_tab(i),
                curr_val_tocny_tab(i),
                sour_code_tab(i),
                customer_type_tab(i)
              );
        --**記錄成功執行的記錄數
        v_success := v_success + sql%rowcount;
        commit;
    END LOOP;
  CLOSE cb_cur;


 -----**調用日誌存儲過程寫入日誌數據
  此處省略。。。

--------------------****** FDM 數據處理完成 *****------------------------------------

exception
  --**總程序異常處理部分
  when others then
    IF cb_cur%ISOPEN
    THEN CLOSE cb_cur;
    END IF;
    begin
      --將錯誤信息插入錯誤日誌表etl_errlog 
      insert into etl_errlog(......省略.....)
      VALUES(......省略......);
      commit;
   end;
end  SP_RF_CB_ACCT;

也可以使用記錄RECORD類型,嵌套表來實現以上功能。

procedure  SP_RF_CB_ACCT(kjrq in varchar2,e_errcode out char) is

 --**定義存儲過程使用的相關變量
  ......此處省略......
  
  --定義CUESOR,此處會獲取到4500萬行左右的記錄
  CURSOR cb_cur IS 
        select  trim(ACCT_NO),
                trim(CURRENCY),
                trim(GL_ACCT_NO),
                trim(BRANCH_NO),
                trim(CUSTOMER_NO),
                trim(SYS_ID),
                trim(ACCT_TYPE),
                OPEN_DATE,
                LST_MNT_DT,
                trim(STATUS),
                utl_raw.cast_to_varchar2(CUSTOMER_NAME) CUSTOMER_NAME,
                CURR_VAL,
                CURR_VAL_TOCNY ,
                trim(SOUR_CODE),
                trim(CUSTOMER_TYPE)
          from v_CB_ACCT@REPORT_RIFM;
            
  --定義基於表類型的變量
  TYPE r_cb_acct IS RECORD(
              ACCT_NO           CB_ACCT.ACCT_NO%TYPE;
              CURRENCY          CB_ACCT.CURRENCY%TYPE;
              GL_ACCT_NO        CB_ACCT.GL_ACCT_NO%TYPE;
              BRANCH_NO         CB_ACCT.BRANCH_NO%TYPE;
              CUSTOMER_NO       CB_ACCT.CUSTOMER_NO%TYPE;
              SYS_ID            CB_ACCT.SYS_ID%TYPE;
              ACCT_TYPE         CB_ACCT.ACCT_TYPE%TYPE;
              OPEN_DATE         CB_ACCT.OPEN_DATE%TYPE;
              LST_MNT_DT        CB_ACCT.LST_MNT_DT%TYPE;
              STATUS            CB_ACCT.STATUS%TYPE;
              CUSTOMER_NAME     CB_ACCT.CUSTOMER_NAME%TYPE;
              CURR_VAL          CB_ACCT.CURR_VAL%TYPE;
              CURR_VAL_TOCNY    CB_ACCT.CURR_VAL_TOCNY%TYPE;
              SOUR_CODE         CB_ACCT.SOUR_CODE%TYPE;
              CUSTOMER_TYPE     CB_ACCT.CUSTOMER_TYPE%TYPE
    );
  
  --定義嵌套表
  TYPE type_cb_acct IS TABLE OF r_cb_acct;
  cb_acct_tab type_cb_acct;
  
  --定義分批插入時,每次插入的最大數據條目,5百萬行
  v_limit pls_integer := 5000000;

begin

  --**刪除目標表的數據
 EXECUTE IMMEDIATE 'truncate table CB_ACCT';

  --**把表的數據加載到目標表中
  OPEN cb_cur;
    LOOP
        FETCH cb_cur BULK COLLECT INTO cb_acct_tab LIMIT v_limit;
          EXIT WHEN cb_acct_tab.COUNT=0;
          
         FORALL i in cb_acct_tab.FIRST..cb_acct_tab.LAST
          INSERT /*+ APPEND */ INTO  CB_ACCT NOLOGGING
              (ACCT_NO,CURRENCY,GL_ACCT_NO,BRANCH_NO,CUSTOMER_NO,SYS_ID, ACCT_TYPE,
                OPEN_DATE,LST_MNT_DT,STATUS,CUSTOMER_NAME,CURR_VAL,CURR_VAL_TOCNY,SOUR_CODE,CUSTOMER_TYPE
              ) values (
                cb_acct_tab(i).ACCT_NO,
                cb_acct_tab(i).CURRENCY,
                cb_acct_tab(i).GL_ACCT_NO,
                cb_acct_tab(i).BRANCH_NO,
                cb_acct_tab(i).CUSTOMER_NO,
                cb_acct_tab(i).SYS_ID,
                cb_acct_tab(i).ACCT_TYPE,
                cb_acct_tab(i).OPEN_DATE,
                cb_acct_tab(i).LST_MNT_DT,
                cb_acct_tab(i).STATUS,
                cb_acct_tab(i).CUSTOMER_NAME,
                cb_acct_tab(i).CURR_VAL,
                cb_acct_tab(i).CURR_VAL_TOCNY,
                cb_acct_tab(i).SOUR_CODE,
                cb_acct_tab(i).CUSTOMER_TYPE
              );
        --**記錄成功執行的記錄數
        v_success := v_success + sql%rowcount;
        commit;
    END LOOP;
  CLOSE cb_cur;


 -----**調用日誌存儲過程寫入日誌數據
  此處省略。。。

--------------------****** FDM 數據處理完成 *****------------------------------------

exception
  --**總程序異常處理部分
  when others then
    IF cb_cur%ISOPEN
    THEN CLOSE cb_cur;
    END IF;
    begin
      --將錯誤信息插入錯誤日誌表etl_errlog 
      insert into etl_errlog(......省略.....)
      VALUES(......省略......);
      commit;
   end;
end  SP_RF_CB_ACCT;

改寫後存儲過程的執行性能大有提升,從之前的耗時一個多小時,優化成了15分鐘內完成,性能提升了75%以上。
優化該存儲過程的關鍵點有:
①大事務改成粒度小的小事務,通過v_limit來限制每次操作的條目數。
②使用批量操作的功能BULK COLLECT … LIMIT。
③使用FORALL來避免SQL引擎和plsql引擎頻繁切換的消耗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章