PL/SQL優化案例之一

有個存儲過程從週五晚上跑了到了週一尚未跑完,存儲過程代碼以下:正則表達式

TMP_NBR_NO_XXXX共有400w行數據,180MB。sql

For in 後面的查詢 select nli.*, .......   and ns2l.nbr_level_id between 201 and 208 order by nl2i.priority; 查詢返回43行數據。express

 

在優化sql的時候,咱們第一眼要找出可能出現性能的地方:oop

一、這個案例邏輯是比較複雜的,loop套loop,這就形成了 笛卡爾乘積,避免少掃描,優化邏輯。(可使用循環表的方式來減小loop循環次數)性能

二、where 條件過濾 是否走索引了,避免全表掃描,fetch

三、select * 要優化掉,避免查詢數據字典優化

四、找到驅動表,儘可能小表在前(這裏跟sql語句不同,CBO 會自動選擇驅動表,可是在plsql裏面loop嵌套loop ,邏輯都固定死了,哪一個在前哪一個就是驅動表)spa

五、內層循環爲啥要排序?有必要嗎?code

六、regexp_like 正則表達式不走索引,優化掉。(業務邏輯須要,這個不能優化)regexp

七、最內層的update 是否有索引?如沒有,那麼將會全表掃描不少次。

八、這裏值得一提的是 開發寫了批量提交,在大量DML操做的時候 這個是一個很好的方法。

九、是否收集了統計信息?(這個不必定,統計信息收集是要根據執行計劃來判斷的,有的sql收集了統計信息反而慢了)

有問題的地方我都圈起來了,以下:

嵌套循環就是一個loop套loop至關於笛卡爾積。該PLSQL代碼中有loop套loop的狀況,這就致使UPDATE TMP_NBR_NO_XXXX要執行400w*43次,TMP_NBR_NO_XXXX.no列沒有索引,TMP_NBR_NO_XXXX每次更新都要進行全表掃描。這就是爲何存儲過程從週五跑到週一還沒跑完的緣由。

有讀者可能會問,爲何不用MERGE進行改寫呢?在PLSQL代碼中是用regexp_like關聯的.沒法走hash鏈接,也沒法走排序合併鏈接,兩表只能走嵌套循環而且被驅動表沒法走索引。若是強行使用MERGE進行改寫,由於該SQL執行時間很長,會致使UNDO不釋放,所以,沒有采用MERGE INTO對代碼進行改寫。

    有讀者可能也會問,爲何不對TMP_NBR_NO_XXXX.no創建索引呢?由於關聯更新能夠採用ROWID批量更新,因此沒有采用創建索引方法優化。

下面採用ROWID批量更新方法改寫上面PLSQL,爲了方便讀者閱讀PLSQL代碼,先建立一個臨時表用於存儲43記錄:

create table TMP_DATE_TEST as

  select  nli.expression, nl.nbr_level_id, priority   from tmp_xxx_item

  ...... and ns2l.nbr_level_id between 201 and 208;

 建立另一個臨時表,用於存儲要被更新的表的ROWID以及no字段:

create table TMP_NBR_NO_XXXX_TEXT as

select rowid rid, nbn.no from TMP_NBR_NO_XXXX nbn

 Where nbn.level_id=1 and length(nbn.no)= 8;  

 改寫以後的PLSQL代碼:

declare
  type rowid_table_type is table of rowid index by pls_integer;
  updateCur sys_refcursor;v_rowid rowid_table_type;
begin
  for c_no_data in (select t.expression, t.nbr_level_id, t.priority   from TMP_DATE_TEST t order by 3) loop
    open updateCur for  select rid   from TMP_NBR_NO_XXXX_TEXT nbn
       where regexp_like(nbn.no, c_no_data.expression);
    loop
      fetch updateCur bulk collect  into v_rowid LIMIT 20000;
      forall i in v_rowid.FIRST .. v_rowid.LAST
      update TMP_NBR_NO_XXXX  set level_id = c_no_data.nbr_level_id   
       where rowid = v_rowid(i); commit;
      exit when updateCur%notfound;
    end loop;
    CLOSE updateCur;
  end loop;
end;

循環嵌套的邏輯改爲了循環表,循環表總比循環嵌套遊標要好的多,改寫後的PLSQL能在4小時左右跑完。有沒有什麼辦法進一步優化呢?單個進程能在4小時左右跑完,若是開啓8個並行進程,那應該能在30分鐘左右跑完。可是PLSQL怎麼開啓並行呢?正常狀況下PLSQL是沒法開啓並行的,若是直接在多個窗口中執行同一個PLSQL代碼,會遇到鎖爭用,若是能解決鎖爭用,在多個窗口中執行同一個PLSQL代碼,這樣就變相實現了PLSQL開並行功能。能夠利用ROWID切片變相實現並行:

select DBMS_ROWID.ROWID_CREATE(1,c.oid,e.RELATIVE_FNO,e.BLOCK_ID,0) minrid,
       DBMS_ROWID.ROWID_CREATE(1,c.oid,e.RELATIVE_FNO,e.BLOCK_ID+e.BLOCKS-1,     10000) maxrid from dba_extents e,
(select max(data_object_id)oid from dba_objects where object_name= 
'TMP_NBR_NO_XXXX_TEXT' and owner='RESXX2')and data_object_id is not null) c
 where e.segment_name='TMP_NBR_NO_XXXX_TEXT'and e.owner = 'RESXX2';

可是這時發現,切割出來的數據分佈嚴重不均衡,這是由於建立表空間的時候沒有指定uniform size 的Extent所致使的。因而新建一個表空間,指定採用uniform size方式管理Extent:

create tablespace TBS_BSS_FIXED datafile '/oradata/bs_bss_fixed_500.dbf' 
       size 500M extent management local uniform size 128k;

重建一個表用來存儲要被更新的ROWID:

create table RID_TABLE
( rowno  NUMBER,  minrid VARCHAR2(18),  maxrid VARCHAR2(18)) ;

將ROWID插入到新表中:

insert into rid_table 
select rownum rowno, 
 DBMS_ROWID.ROWID_CREATE(1,c.oid,e.RELATIVE_FNO,e.BLOCK_ID, 0) minrid,
 DBMS_ROWID.ROWID_CREATE(1,c.oid,e.RELATIVE_FNO,e.BLOCK_ID + e.BLOCKS - 1, 10000) maxrid  from dba_extents e,
(select max(data_object_id)oid from dba_objects where object_name= 
'TMP_NBR_NO_XXXX_TEXT' and owner='RESXX2')and data_object_id is not null) c
 where e.segment_name='TMP_NBR_NO_XXXX_TEXT'and e.owner = 'RESXX2';

 這樣RID_TABLE中每行指定的數據都很均衡,大概4035條數據。最終更改的PLSQL代碼:

create or replace  procedure  pro_phone_grade(flag_num in number)
as 
 type rowid_table_type is table of  rowid index  by  pls_integer;  
 updateCur  sys_refcursor;v_rowid  rowid_table_type;
 v_rowid2  rowid_table_type;
begin
for  rowid_cur in (select  *  from  rid_table  where mod(rowno, 8)=flag_num
 loop
    for c_no_data in (select t.expression, t.nbr_level_id, t.priority  from TMP_DATE_TEST t order by 3 ) 
       loop
         open  updateCur  for  select rid,rowid  from TMP_NBR_NO_XXXX_TEXT  nbn
           where rowid between rowid_cur.minrid and rowid_cur.maxrid  
          and regexp_like(nbn.no, c_no_data.expression);
          loop
            fetch updateCur  bulk collect  into  v_rowid, v_rowid2  LIMIT 20000;
              forall i in v_rowid.FIRST ..v_rowid.LAST
              update TMP_NBR_NO_XXXX  set  level_id = c_no_data.nbr_level_id          
               where rowid = v_rowid(i);    commit;           
            exit when  updateCur%notfound;
         end loop; 
         CLOSE updateCur; 
       end loop;
   end loop;
end;

而後在8個窗口中同時運行上面PLSQL代碼:

Begin  pro_phone_grade(0); end; ..... Begin pro_phone_grade(7); end;

最終能在29分左右跑完全部存儲過程。本案例技巧就在於ROWID切片實現並行,而且考慮到了數據分佈對並行的影響,其次還使用了ROWID關聯更新技巧。

相關文章
相關標籤/搜索