開發人員找到我說,報表系統中有一個存儲過程最近總是報錯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引擎頻繁切換的消耗。