1. 程式人生 > >一波三折:UPDATE語句改寫優化

一波三折: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表空間的大小來決定吧。