temp表空間被過多占用處理方法


這個步驟比較簡單,查詢v$sort_usage就可以了:

select * from   
  1. (select username,session_addr,sql_id,contents,segtype,blocks*8/1024/1024 gb   
  2. from v$sort_usage order by blocks desc)   
  3. where rownum<=200;  
  4.   
  5. USERNAME    SESSION_ADDR     SQL_ID        CONTENTS  SEGTYPE            GB  
  6. ----------  ---------------- ------------- --------- --------- -----------  
  7. XXXX        0700002949BCD8A0 291nk7db4bwdh TEMPORARY SORT      .9677734375  
  8. XXXX        070000294BD99628 291nk7db4bwdh TEMPORARY SORT      .9677734375  
  9. XXXX        070000294CD10480 291nk7db4bwdh TEMPORARY SORT      .9677734375  
  10. XXXX        070000294DD1AC88 291nk7db4bwdh TEMPORARY SORT      .9677734375  
  11. XXXX        070000294CD68D70 291nk7db4bwdh TEMPORARY SORT      .9677734375  
  12. XXXX        070000294DBDF760 291nk7db4bwdh TEMPORARY SORT      .9677734375  
  13. XXXX        070000294EDB5D10 291nk7db4bwdh TEMPORARY SORT      .9677734375  
  14. XXXX        070000294FD7D818 291nk7db4bwdh TEMPORARY SORT      .9677734375  
  15. ...結果較多,忽略部分輸出...  

SQL_ID都是一樣的,那這個SQL是否有其特殊性呢?SEGTYPE為SORT表明這個臨時段是“排序段”,用於SQL排序,大小居然也是一樣,會話占用的臨時段大小將近1GB,幾百個會話加在一起,想不讓臨時表空間不撐大都難。

看看這個相同的SQL ID代表的SQL是什么:

SQL> @sqlbyid 291nk7db4bwdh  
  1.   
  2. SQL_FULLTEXT  
  3. --------------------------------------------------------------------------------------------------------------  
  4.  SELECT  A.LLEVEL,  A.LMODE  FROM TABLE_XXX A  WHERE A.SERVICE_NAME = :SERVICE_NAME AND STATE='Y'  

很明顯,這是一條非常簡單的SQL,沒有ORDER BY ,也沒有GROUP BY、UNION、DISTINCT等需要排序的,TABLE_XXX是一張普通的表,而不是視圖。出現了什么問題?會不會是v$sort_usage的 SQL_ID列有錯誤?我們查看其中一個會話正在執行的SQL:

select sid,prev_sql_id, sql_id from v$session where saddr='070000294AC0D050';  
  1.   
  2.         SID PREV_SQL_ID   SQL_ID  
  3. ----------- ------------- -------------  
  4.        3163 291nk7db4bwdh  

v$sort_usage中看到某個會話當前沒有執行任何SQL,v$sort_usage中的SQL_ID是該會話前一條執行的SQL。為什么這里顯示的是會話前一條執行的SQL,關於這個問題后面再詳述,但至少有一點是可以判斷的:如果大量的臨時段都是由會話當前正在執行的SQL所產生的,那說明同時有幾百個會話在執行需要大量臨時空間的SQL,那系統早就崩潰了。所以這些臨時表空間的占用不應該是由當前在執行的SQL所產生的,至少大部分不是。

大部分人的一個錯誤觀點是,臨時表空間中當前占用的空間是由會話當前正在執行的SQL所產生的。上面的一個簡單的分析判斷,情況不應該是這樣。我們可以基於查詢類SQL的執行過程來分析:

  1. 解析SQL語句(Parse),生成一個游標(Open Cursor)。
  2. 執行SQL語句(Execute),嚴格說就是執行新產生的游標。
  3. 在游標中取數據(Fetch)。
  4. 關閉游標(Close Cursor)。

關鍵在第3步。大家都知道取數據有一個array size的概念,表示一次從游標中取多少條數據,這是一個循環的過程。如果SQL查詢得到的數據有1000條,每次取100條,則需要取10次。對於Fetch Cursor,有兩點:

  1. 一個游標,或者說一條SQL語句,並不要求客戶端把所有數據取完,只取了一部分數據就關閉游標也是可以的。
  2. 只要還沒有關閉游標,數據庫就要維護該游標的狀態,如果是排序的SQL,也需要維持該SQL已經排好序的數據。

很顯然,從上述第2點可以知道,如果一條SQL使用了臨時段來排序,在SQL對應的游標沒關閉的情況下,Oracle數據庫不會去釋放臨時段,因為對於Oracle數據庫來說,它不會知道客戶端是否還要繼續取游標的數據。

基於這樣的分析,我們只需要隨便選擇一個占用了接近1GB的會話,查詢v$open_cursor,查看其打開的游標中是否有大數據量排序的SQL:

 
  1. SQL> select sql_id,sorts,rows_processed/executions from v$sql  
  2.   2  where parsing_schema_name='ACCT' and executions>0 and sorts>0  
  3.   3  and sql_id in (select sql_id from v$open_cursor where sid=4505)  
  4.   4  order by 3;  
  5.     
  6.   SQL_ID              SORTS ROWS_PROCESSED/EXECUTIONS  
  7. ------------- ----------- -------------------------  
  8. ...省略部分輸出結果...  
  9. 86vp997jbz7s6       63283                       593  
  10. cfpdpb526ad43         592               35859.79899  
  11. cfpdpb526ad43         188               55893.61702  
  12. cfpdpb526ad43         443                     71000  

 

最后三個游標,實際上都是同一條SQL語句,排序的數據量最大,我們來看看這條SQL是什么:

@sqlbyid cfpdpb526ad43  
  1.   
  2. SQL_FULLTEXT  
  3. ---------------------------------------------------------------------------------------------------  
  4. select ... from  c, b, a, d, e where ... order by d.billing_cycle_id desc,e.offer_name,a.acc_name  

基於為客戶保密的原因,SQL做了處理,能知道這條SQL的確是排了序就行,不過在SQL中看不出來的是,這條SQL沒有任何實質性的能夠過濾大量數據的條件。那么我們count(*)這條SQL語句看看:

COUNT(*)  
  1. --------  
  2. 12122698  

出來的結果居然有1200多萬條數據,一個前台應用,不知道取1200多萬條數據干嘛。但是從rows_processed/executions 只有幾萬的結果來看,應用在取了幾萬條數據之后,由於某些原因(最大的可能就是不能再處理更多的數據),不再繼續取數據,但是游標也一直沒有關閉。

比較容易就能進行演示sort by時臨時表空間的占用。

 
  1. 根據dba_objects建一個測試表T1,使其數據量達到2000萬行。  
  2.  select count(*) from t1;  
  3.   
  4.    COUNT(*)  
  5. -----------  
  6.    20171200  
  7.    
  8. 然后將SQL工作區設置為手動模式,設置sort內存大小限制為200M:  
  9.  alter session set workarea_size_policy=manual;  
  10.  alter session set sort_area_size=209715200;  
  11.   
  12. 查詢得到當前的會話sid:  
  13.  select sid from v$mystat where rownum< =1;  
  14.   
  15.         SID  
  16. -----------  
  17.        2111  
  18.   
  19. 執行這下面的代碼:  
  20.  declare  
  21.   2     v_object_name varchar2(100);  
  22.   3     v_dummy varchar2(100);  
  23.   4  begin  
  24.   5    for rec in (select * from t1 order by object_id,object_name) loop  
  25.   6       select object_type into v_dummy from t1 where rownum<=1;  
  26.   7       select object_name into v_object_name from dba_objects where object_id=rec.object_id;  
  27.   8       dbms_lock.sleep(60*10);  
  28.   9       exit;  
  29.  10    end loop;  
  30.  11  end;  
  31.  12  /  
  32. 這段代碼會打開一個游標,對2000萬的數據量進行排序,然后在循環中只取一條數據,然后就進入sleep。在另一個窗口中監控到2111這個會話的event變成了PL/SQL lock timer,就去查詢v$sort_usage:  
  33. select a.sql_id sort_sql_id,b.sql_id,b.prev_sql_id, contents,segtype,blocks*8/1024/1024 gb   
  34.   2  from v$sort_usage a,v$session b   
  35.   3  where a.session_addr=b.saddr  
  36.   4  and b.sid=2111;  
  37.   
  38. SORT_SQL_ID   SQL_ID        PREV_SQL_ID   CONTENTS  SEGTYPE            GB  
  39. ------------- ------------- ------------- --------- --------- -----------  
  40. fabh24prgk2sj bhzf316mdc07w fabh24prgk2sj TEMPORARY SORT      1.444824219  
  41. 可以看到v$sort_usage中的SQL_ID(即上述結果中SORT_SQL_ID)與v$session中的pre_sql_id一致,這條SQL是:  
  42.   
  43. @sqlbyid fabh24prgk2sj  
  44. SQL_FULLTEXT  
  45. --------------------------------------------------------  
  46. SELECT OBJECT_NAME FROM DBA_OBJECTS WHERE OBJECT_ID=:B1  
  47.   
  48. 而實際上當前正在執行的SQL是:  
  49.  @sqlbyid bhzf316mdc07w  
  50.   
  51. SQL_FULLTEXT  
  52. ---------------------------------------------------------------------------  
  53. declare  
  54.    v_object_name varchar2(100);  
  55.    v_dummy varchar2(100);  
  56. begin  
  57.   for rec in (select * from t1 order by object_id,object_name) loop  
  58.      select object_type into v_dummy from t1 where rownum<=1;  
  59.      select object_name into v_object_name from dba_objects where object_id=rec.object_id;  
  60.      dbms_lock.sleep(60*10);  
  61.      exit;  
  62.   end loop;  
  63. end;  

問題分析到這里,很明顯確認的是,應用存在問題,也許是業務邏輯問題;也許是根據前台選擇的條件拼接的SQL,但是沒有任何條件時就查詢了所有數 據。接下來就是找來開發人員,至於后面的事就跟這個主題沒有太大關系。我們可以根據這個案例來進一步展開,去探尋臨時表空間的更多知識點。

這里要展開的第1點是,v$sort_usage中的sql_id是不是會話正在執行的SQL,我們去看看視圖fixed_View_definition就知道了:

 
  1. select x$ktsso.inst_id, username, username, ktssoses, ktssosno, prev_sql_addr, prev_hash_value,  
  2. prev_sql_id, ktssotsn, decode(ktssocnt, 0, 'PERMANENT', 1, 'TEMPORARY'), decode(ktssosegt, 1,  
  3. 'SORT', 2, 'HASH', 3, 'DATA', 4, 'INDEX', 5, 'LOB_DATA', 6, 'LOB_INDEX' , 'UNDEFINED'), ktssofno,  
  4. ktssobno, ktssoexts, ktssoblks, ktssorfno from x$ktsso, v$session where ktssoses = v$session.saddr  
  5. and ktssosno = v$session.serial#  

原來在v$sort_usage的定義中,就明確地說明了SQL_ID列是v$session中的prev_sql_id列,而不是當前的SQL。至於為什么這樣定義,老實說,現在還不知道。

不過從11.2.0.2這個版本開始,v$sort_usage的基表x$ktsso中增加了一個字段ktssosqlid,表示該臨時段真正關聯的SQL,以上述的測試結果為例,查詢這個基表的結果如下:

select ktssosqlid from x$ktsso, v$session where ktssoses = v$session.saddr  
  1.   2  and ktssosno = v$session.serial#  
  2.   3  and v$session.sid=2111;  
  3.   
  4. KTSSOSQLID  
  5. -------------  
  6. 60t6fmjsw6v8y  
  7.   
  8. @sqlbyid 60t6fmjsw6v8y  
  9.   
  10. SQL_FULLTEXT  
  11. ---------------------------------------------------------------------------  
  12. SELECT * FROM T1 ORDER BY OBJECT_ID,OBJECT_NAME  

可以看到的是我們查詢到了真正產生臨時段的SQL。

一直以來,v$sort_usage中的SQL_ID誤導了很多人。所幸的是Oracle從11.2.0.2開始進行了彌補,MOS中有文檔:

Bug 17834663 - Include SQL ID for statement that created a temporary segment in GV$SORT_USAGE (文檔 ID 17834663.8)
In previous versions, it was not possible to identify the SQL ID
of the statement that created a given temporary segment in
eg. (G)V$SORT_USAGE.

@ Via the fix for bug:8806817 we added the SQL ID to the X$KTSSO
@ table (ktssosqlid), but it was not exposed in the GV$SORT_USAGE
@ view until now.

The SQL ID of the statement is in column SQL_ID_TEMPSEG

Note that this fix cannot be provided as an interim patch.

我們改良一下v$sort_usage,使用如下的查詢來代替:

select k.inst_id "INST_ID",  
  1.        ktssoses "SADDR",  
  2.        sid,  
  3.        ktssosno "SERIAL#",  
  4.        username "USERNAME",  
  5.        osuser "OSUSER",   
  6.        ktssosqlid "SQL_ID",  
  7.        ktssotsn "TABLESPACE",  
  8.        decode(ktssocnt, 0, 'PERMANENT', 1, 'TEMPORARY') "CONTENTS",  
  9.        --注意在12c的v$sort_usage定義中TABLESPACE和CONTENTS已經發生變化了。  
  10.        decode(ktssosegt, 1, 'SORT', 2, 'HASH', 3, 'DATA', 4, 'INDEX',   
  11.           5, 'LOB_DATA', 6, 'LOB_INDEX' , 'UNDEFINED') "SEGTYPE",  
  12.        ktssofno "SEGFILE#",  
  13.        ktssobno "SEGBLK#",  
  14.        ktssoexts "EXTENTS",  
  15.        ktssoblks "BLOCKS",  
  16.        round(ktssoblks*p.value/1024/1024, 2) "SIZE_MB",  
  17.        ktssorfno "SEGRFNO#"  
  18. from x$ktsso k, v$session s,   
  19.      (select value from v$parameter where name='db_block_size') p   
  20. where ktssoses = s.saddr  
  21.   and ktssosno = s.serial#;  

要展開的第2點是,v$sort_usage中的SEGTYPE列的不同的值各有什么意義:

  1. SORT:SQL排序使用的臨時段,包括order by、group by、union、distinct、窗口函數(window function)、建索引等產生的排序。
  2. DATA:臨時表(Global Temporary Table)存儲數據使有的段。
  3. INDEX:臨時表上建的索引使用的段。
  4. HASH:hash算法,如hash連接所使用的臨時段。
  5. LOB_DATA和LOB_INDEX:臨時LOB使用的臨時段。

根據上述的段類型,大體可以分為三類占用:

  1. SQL語句排序、HASH JOIN占用
  2. 臨時表占用
  3. 臨時LOB對象占用

臨時表空間的異常占用,一種緩步增長的,另一種情況:一下撐滿的通常是一個極大數據量的排序或極大的索引的創建。緩步增長的情況,跟系統的內存被逐 漸占用類似,存在“泄露”。比如排序的SQL游標沒有關閉,比如本文的案例;比如會話級臨時表產生了數據后一直沒有清除;臨時LOB對象沒有清理或泄露。 前兩種比較好去分析處理,但是臨時LOB的泄露問題就復雜很多。

來看一個測試:

  select sid from v$mystat where rownum<=1;  
  1.   
  2.         SID  
  3. -----------  
  4.        1773  
  5.  declare  
  6.   2    v_lob clob;  
  7.   3  begin  
  8.   4    dbms_lob.createtemporary(v_lob,true);  
  9.   5    dbms_lob.writeappend(v_lob,1000,lpad('a',1000,'a'));  
  10.   6  end;  
  11.   7  /  

上述的代碼執行完之后,在另一個窗口中,我們查詢v$sort_usage:

 
  1. select a.sql_id sort_sql_id,b.sql_id,b.prev_sql_id, contents,segtype,blocks*8/1024/1024 gb   
  2.   2  from v$sort_usage a,v$session b   
  3.   3  where a.session_addr=b.saddr  
  4.   4  and b.sid=1773;  
  5.   
  6. SORT_SQL_ID   SQL_ID        PREV_SQL_ID   CONTENTS  SEGTYPE            GB  
  7. ------------- ------------- ------------- --------- --------- -----------  
  8. 9babjv8yq8ru3               9babjv8yq8ru3 TEMPORARY LOB_DATA  .0004882813  
  9.   
  10. @sqlbyid 9babjv8yq8ru3  
  11.   
  12. SQL_FULLTEXT  
  13. ---------------------------------------------------------------------------  
  14. BEGIN DBMS_OUTPUT.GET_LINES(:LINES, :NUMLINES); END;  

可以看到,這個會話已經產生了類型為LOB_DATA的臨時段。雖然SQL代碼已經執行完成,會話已經處於空閑狀態,但是臨時段仍然存在着。

Oracle中的LOB變量,類似於C語句中的指針,或者類似於JAVA代碼中的數據庫連接Connection,是需要釋放的。上述有問題的代 碼,缺少了釋放LOB的代碼:dbms_log.freetemporary(v_lob)。好在對於這種情況,Oracle提供了一個補救措施,就是設 置60025事件可以自動清理掉不活動的LOB,只需要在參數文件中加上event='60025 trace name context forever'。

在Oracle數據庫中,xmltype類型內部也實際上是LOB類型,xmltype類型的數據操作可能會產生較多的LOB臨時段。lob類型的 字段上的更改操作,比如lob拼接等,同樣會產生LOB臨時段。如果在v$sort_usage中發現大量的LOB類型的臨時段,那么通常是由於代碼存在 問題,沒有釋放LOB,或者是由於Oracle本身的BUG。在MOS上,如果以lob temporary關鍵字搜索,會發現相當多的關於lob臨時段的泄露或臨時段沒有釋放相關的文檔。

最后,不管是什么情況導致的臨時表空間被過多占用,通常重啟應用能夠釋放掉臨時段,因為會話退出后,相對應的臨時段就會被釋放。看來,“重啟”大法在這種情況下就很有用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM