一波三折:UPDATE語句改寫優化
最近趕上第四季度上版。很多套系統遷移測試,太忙,抽空帶徒弟去優化了一條UPDATE改寫的SQL
具體的故障分析報告是她寫的,如下:
2018年10月份的一天,歷史報表系統的開發人員讓我幫忙優化一個每天執行報錯ORA-01555的儲存過程,由於最近給他們系統處理過其他的幾個儲存過程,優化後的效率都得到了大幅度提升,只是本人比較懶,沒有儲存下來優化前後的指令碼以及優化思路,聽領導說本月底要進行技術分享,於是就想總結下這個儲存過程的優化過程,也算是應付月底的技術分享會議了,哈哈^.^。
第一步:先來欣賞一下2017年7月份之前的原始SQL的寫法:
UPDATE R_AAAA_AA A SET (JBBR_CD, JS_BAL, YEBZ_FLG) = (SELECT M.BRANCH_NO, NVL(M.CURR_VAL, 0), CASE WHEN NVL(M.CURR_VAL, 0) < NVL(A.BAK4, 0) THEN 0 ELSE 1 END FROM M_BBB_BB M WHERE SUBSTR(A.JS_NO, 1, 16) = M.ACCT_NO AND M.EXTDATE = TO_DATE(kjrq, 'YYYYMMDD')) WHERE A.DT_COMMIT = kjrq;
先來查詢下R_AAAA_AA, M_BBB_BB 兩個表的資料量:
select count(1) from R_AAAA_AA; --18848326行
select count(1) from M_BBB_BB ; --34920442行,開發人員說該表的資料量比較固定,每天都是3500萬行左右
再來看一下原始SQL的真實執行計劃:
Plan hash value: 269157309 --------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------------------------- | 0 | UPDATE STATEMENT | | 6408 | 269K| 157K (13)| 00:31:35 | | 1 | UPDATE | R_AAAA_AA | | | | | |* 2 | TABLE ACCESS FULL | R_AAAA_AA | 6408 | 269K| 42527 (1)| 00:08:31 | |* 3 | TABLE ACCESS BY INDEX ROWID| M_BBB_BB | 1 | 33 | 5 (0)| 00:00:01 | |* 4 | INDEX RANGE SCAN | IDX_M_BBB_BB | 1 | | 4 (0)| 00:00:01 | --------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("A"."DT_COMMIT"='20181010') 3 - filter("M"."ACCT_NO"=SUBSTR(:B1,1,16)) 4 - access("M"."EXTDATE"=TO_DATE(' 2018-10-10 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
瞭解表關聯方式的同學應該知道,UPDATE後面跟子查詢類似巢狀迴圈,它的演算法與標量子查詢,Filter一模一樣。
也就是說表M_BBB_BB 相當於巢狀迴圈的被驅動表,而走了索引範圍掃描INDEX RANGE SCAN,相當於該表被掃描了18848326次,這是比較坑的,基本上2個小時是無法出結果的,另外一個比較坑的是,索引範圍掃描後的TABLE ACCESS BY INDEX ROWID回表,回表是單塊讀,這裡要消耗多少物理讀和邏輯讀啊,想想還是比較恐怖的,綜合這兩個大坑,此條SQL的原始寫法,必然導致它每天報錯ORA-01555。
在2018年7月份的某天我們開發DBA組的同事用MERGE INTO對此SQL進行了改寫,先來講下merge into的使用場景,原理和優點:
場景:多表關聯後,對主表進行update或insert操作時使用。
原理:從using搜出來的結果逐條與on條件匹配,然後決定是update還是insert。檔using後面的SQL沒有查詢到資料時,
merge into 語句是不會執行update和insert操作的。
格式:懶得寫了,想了解的,自行百度吧。
第二步:來看一看最近的報錯資訊,我直接截圖了:
由上圖Query Duration=4437 sec可知,該條語句執行了一個多小時,沒有執行完畢就報錯了。我找到了它的執行計劃,如下:
SQL_ID 1c6x55j4a1u92, child number 0
-------------------------------------
MERGE INTO R_AAAA_AAA USING (SELECT M.BRANCH_NO,
NVL(M.CURR_VAL, 0) CURR_VAL, M.ACCT_NO FROM M_BBB_BB M WHERE
M.EXTDATE = TO_DATE(:B1 , 'YYYYMMDD')) M ON (SUBSTR(A.JS_NO, 1, 16) =
M.ACCT_NO) WHEN MATCHED THEN UPDATE SET JBBR_CD = M.BRANCH_NO, JS_BAL =
M.CURR_VAL, YEBZ_FLG = CASE WHEN NVL(M.CURR_VAL, 0) < NVL(A.BAK4, 0)
THEN 0 ELSE 1 END
Plan hash value: 938640974
-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | MERGE STATEMENT | | | | 42715 (100)| |
| 1 | MERGE | R_AAAA_AAA | | | | |
| 2 | VIEW | | | | | |
|* 3 | HASH JOIN | | 1 | 146 | 42715 (1)| 00:08:33 |
| 4 | TABLE ACCESS BY INDEX ROWID| M_BBB_BB | 1 | 34 | 5 (0)| 00:00:01 |
|* 5 | INDEX RANGE SCAN | IDX_M_BBB_BB | 1 | | 4 (0)| 00:00:01 |
| 6 | TABLE ACCESS FULL | R_AAAA_AAA | 9542K| 1019M| 42658 (1)| 00:08:32 |
-------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("M"."ACCT_NO"=SUBSTR("A"."JS_NO",1,16))
5 - access("M"."EXTDATE"=TO_DATE(:B1,'YYYYMMDD'))
有執行計劃分析可知,兩表關聯方式為HASH連線,驅動表雖然通過INDEX RANGE SCAN索引範圍掃描來訪問表,但是該表的資料量可是3500萬行,回表一不小心就要回表3500萬次,回表在訪問UNDO表空間時,找不到原來的資料,自然就會報錯ORA-01555了。
話說回來,MERGE INTO的好處還是不少的,只是此處不該有回表操作,所以考慮避免回表,讓訪問表FDM_INVM時可以走全表掃描。關於這個想法可以執行一下,看看效率再說,具體執行計劃如下:(此種改寫方法執行時間只消耗了400多秒,不到7分鐘)
MERGE/*+ use_hash(a,b) leading(a) */ INTO R_AAAA_AAA A
USING (SELECT /*+ full(M) qb_name(b) */ M.BRANCH_NO,
NVL(M.CURR_VAL, 0) CURR_VAL,
M.ACCT_NO
FROM M_BBB_BB M
WHERE M.EXTDATE = TO_DATE('20181023', 'YYYYMMDD')) mm
ON (SUBSTR(A.JS_NO, 1, 16) = mm.ACCT_NO)
WHEN MATCHED THEN
UPDATE
SET JBBR_CD = mm.BRANCH_NO,
JS_BAL = mm.CURR_VAL,
YEBZ_FLG = CASE
WHEN NVL(mm.CURR_VAL, 0) < NVL(A.BAK4, 0) THEN
0
ELSE
1
END;
-------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------
| 0 | MERGE STATEMENT | | 1 | 64 | 1023K (1)| 03:24:39 |
| 1 | MERGE | R_AAAA_AAA | | | | |
| 2 | VIEW | | | | | |
|* 3 | HASH JOIN | | 1 | 64 | 1023K (1)| 03:24:39 |
| 4 | TABLE ACCESS FULL| R_AAAA_AAA | 9542K| 272M | 42658 (1)| 00:08:32 |
|* 5 | TABLE ACCESS FULL| M_BBB_BB | 1 | 34 | 961K (1)| 03:12:20 |
-------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("MM"."ACCT_NO"=SUBSTR("A"."JS_NO",1,16))
5 - filter("M"."EXTDATE"=TO_DATE(' 2018-10-23 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
MERGE INTO可以自由控制表關聯方式走巢狀迴圈或是HASH連線,而且MERGE INTO可以開啟並行DML,並行查詢,
但資料量大些,這樣的更新也是個大事務,對UNDO的佔用較大較長,只有到最後COMMIT的時候才會釋放UNDO空間;
另外程序突然斷開連線,MERGE INTO更新連線就會導致UNDO很難釋放。但是我們可以考慮PLSQL語言進行批量更新,
採用PLSQL更新不需要擔心程序突然斷開連線,對UNDO佔用也小,相對而言更新過程比較安全、平穩。
第三步:就是PLSQL批量更新的寫法,把這段程式碼放在原有的儲存過程中執行,據說第二天跑批時只花了23分鐘跑完了整個儲存過程,整個儲存過程裡不僅有刪除,insert,查詢,還有這個update,這個批量更新耗時不到10分鐘。改寫前整個儲存過程每天都因報錯停止一次,重跑還要一個多小時才能完成,改寫完成後,第二天跑批沒有再報錯,也無需再重跑,因此這個效率的提升還是很可喜的。
declare
--加上HINT: /*+ full(m) use_hash(a,m) leading(a)*/ 這個查詢執行時間是3分30秒, 不加執行了7分鐘都沒出結果
CURSOR cur_update is
select/*+ use_hash(A,M) */ A.rowid row_id, M.branch_no,nvl(M.CURR_VAL,0) curr_val, M.acct_no
from R_AAAA_AAA A, M_BBB_BB M
where SUBSTR(A.js_no,1,16) = M.acct_no
and M.extdate = to_date(:ywdate, 'YYYYMMDD') order by A.rowid;
TYPE rec_invm_type IS RECORD(
row_id varchar2(50),
branch_no M_BBB_BB.branch_no%TYPE,
curr_val M_BBB_BB.curr_val%TYPE,
acct_no M_BBB_BB.acct_no%TYPE
) ;
TYPE ty_invm IS TABLE OF rec_invm_type;
tab_invm ty_invm;
begin
OPEN cur_update;
LOOP
FETCH cur_update BULK COLLECT INTO tab_invm LIMIT 2000000;
EXIT WHEN tab_invm.COUNT = 0;
FORALL i in tab_invm.FIRST..tab_invm.LAST
update R_AAAA_AAA R
set JBBR_CD = tab_invm(i).branch_no,
JS_BAL = tab_invm(i).curr_val,
YEBZ_FLG = case when nvl(tab_invm(i).curr_val, 0) < nvl(R.BAK4, 0) then
0
else
1
END
where R.rowid = tab_invm(i).row_id;
commit;
END LOOP;
CLOSE cur_update;
end;
以上儲存過程中,在定義CURSOR時,兩表關聯的執行計劃如下;
--加上full(m) 執行時間是3:30秒, 不加執行了5分鐘不出結果
select /*+ full(m) use_hash(a,m) leading(a)*/
a.rowid row_id, m.branch_no, nvl(m.curr_val, 0) curr_val, m.acct_no
from R_AAAA_AAA a, M_BBB_BB m
where substr(a.js_no, 1, 16) = m.acct_no
and m.extdate = to_date('20181023', 'yyyymmdd')
order by a.rowid;
Plan hash value: 1480194564
--------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 64 | | 1023K (1)| 03:24:39 |
| 1 | SORT ORDER BY | | 1 | 64 | | 1023K (1)| 03:24:39 |
|* 2 | HASH JOIN | | 1 | 64 | 382M| 1023K (1)| 03:24:39 |
| 3 | TABLE ACCESS FULL| R_AAAA_AAA | 9542K| 272M| | 42585 (1)| 00:08:32 |
|* 4 | TABLE ACCESS FULL| M_BBB_BB | 1 | 34 | | 961K (1)| 03:12:20 |
--------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("M"."ACCT_NO"=SUBSTR("A"."JS_NO",1,16))
4 - filter("M"."EXTDATE"=TO_DATE(' 2018-10-23 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
這個查詢的瓶頸也是回表,增加HINT後,避免回表,同時走HASH連線,大大的提升了執行效率。
到此,對於UPDATE的改寫,是選擇MERGE INTO還是PLSQL批量更新,讀者可以根據自己資料量,以及UNDO表空間的大小來決定吧。