這個步驟比較簡單,查詢v$sort_usage就可以了:
- (select username,session_addr,sql_id,contents,segtype,blocks*8/1024/1024 gb
- from v$sort_usage order by blocks desc)
- where rownum<=200;
- USERNAME SESSION_ADDR SQL_ID CONTENTS SEGTYPE GB
- ---------- ---------------- ------------- --------- --------- -----------
- XXXX 0700002949BCD8A0 291nk7db4bwdh TEMPORARY SORT .9677734375
- XXXX 070000294BD99628 291nk7db4bwdh TEMPORARY SORT .9677734375
- XXXX 070000294CD10480 291nk7db4bwdh TEMPORARY SORT .9677734375
- XXXX 070000294DD1AC88 291nk7db4bwdh TEMPORARY SORT .9677734375
- XXXX 070000294CD68D70 291nk7db4bwdh TEMPORARY SORT .9677734375
- XXXX 070000294DBDF760 291nk7db4bwdh TEMPORARY SORT .9677734375
- XXXX 070000294EDB5D10 291nk7db4bwdh TEMPORARY SORT .9677734375
- XXXX 070000294FD7D818 291nk7db4bwdh TEMPORARY SORT .9677734375
- ...結果較多,忽略部分輸出...
SQL_ID都是一樣的,那這個SQL是否有其特殊性呢?SEGTYPE為SORT表明這個臨時段是“排序段”,用於SQL排序,大小居然也是一樣,會話占用的臨時段大小將近1GB,幾百個會話加在一起,想不讓臨時表空間不撐大都難。
看看這個相同的SQL ID代表的SQL是什么:
- SQL_FULLTEXT
- --------------------------------------------------------------------------------------------------------------
- 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:
- SID PREV_SQL_ID SQL_ID
- ----------- ------------- -------------
- 3163 291nk7db4bwdh
v$sort_usage中看到某個會話當前沒有執行任何SQL,v$sort_usage中的SQL_ID是該會話前一條執行的SQL。為什么這里顯示的是會話前一條執行的SQL,關於這個問題后面再詳述,但至少有一點是可以判斷的:如果大量的臨時段都是由會話當前正在執行的SQL所產生的,那說明同時有幾百個會話在執行需要大量臨時空間的SQL,那系統早就崩潰了。所以這些臨時表空間的占用不應該是由當前在執行的SQL所產生的,至少大部分不是。
大部分人的一個錯誤觀點是,臨時表空間中當前占用的空間是由會話當前正在執行的SQL所產生的。上面的一個簡單的分析判斷,情況不應該是這樣。我們可以基於查詢類SQL的執行過程來分析:
- 解析SQL語句(Parse),生成一個游標(Open Cursor)。
- 執行SQL語句(Execute),嚴格說就是執行新產生的游標。
- 在游標中取數據(Fetch)。
- 關閉游標(Close Cursor)。
關鍵在第3步。大家都知道取數據有一個array size的概念,表示一次從游標中取多少條數據,這是一個循環的過程。如果SQL查詢得到的數據有1000條,每次取100條,則需要取10次。對於Fetch Cursor,有兩點:
- 一個游標,或者說一條SQL語句,並不要求客戶端把所有數據取完,只取了一部分數據就關閉游標也是可以的。
- 只要還沒有關閉游標,數據庫就要維護該游標的狀態,如果是排序的SQL,也需要維持該SQL已經排好序的數據。
很顯然,從上述第2點可以知道,如果一條SQL使用了臨時段來排序,在SQL對應的游標沒關閉的情況下,Oracle數據庫不會去釋放臨時段,因為對於Oracle數據庫來說,它不會知道客戶端是否還要繼續取游標的數據。
基於這樣的分析,我們只需要隨便選擇一個占用了接近1GB的會話,查詢v$open_cursor,查看其打開的游標中是否有大數據量排序的SQL:
- SQL> select sql_id,sorts,rows_processed/executions from v$sql
- 2 where parsing_schema_name='ACCT' and executions>0 and sorts>0
- 3 and sql_id in (select sql_id from v$open_cursor where sid=4505)
- 4 order by 3;
- SQL_ID SORTS ROWS_PROCESSED/EXECUTIONS
- ------------- ----------- -------------------------
- ...省略部分輸出結果...
- 86vp997jbz7s6 63283 593
- cfpdpb526ad43 592 35859.79899
- cfpdpb526ad43 188 55893.61702
- cfpdpb526ad43 443 71000
最后三個游標,實際上都是同一條SQL語句,排序的數據量最大,我們來看看這條SQL是什么:
- SQL_FULLTEXT
- ---------------------------------------------------------------------------------------------------
- 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語句看看:
- --------
- 12122698
出來的結果居然有1200多萬條數據,一個前台應用,不知道取1200多萬條數據干嘛。但是從rows_processed/executions 只有幾萬的結果來看,應用在取了幾萬條數據之后,由於某些原因(最大的可能就是不能再處理更多的數據),不再繼續取數據,但是游標也一直沒有關閉。
比較容易就能進行演示sort by時臨時表空間的占用。
- 根據dba_objects建一個測試表T1,使其數據量達到2000萬行。
- select count(*) from t1;
- COUNT(*)
- -----------
- 20171200
- 然后將SQL工作區設置為手動模式,設置sort內存大小限制為200M:
- alter session set workarea_size_policy=manual;
- alter session set sort_area_size=209715200;
- 查詢得到當前的會話sid:
- select sid from v$mystat where rownum< =1;
- SID
- -----------
- 2111
- 執行這下面的代碼:
- declare
- 2 v_object_name varchar2(100);
- 3 v_dummy varchar2(100);
- 4 begin
- 5 for rec in (select * from t1 order by object_id,object_name) loop
- 6 select object_type into v_dummy from t1 where rownum<=1;
- 7 select object_name into v_object_name from dba_objects where object_id=rec.object_id;
- 8 dbms_lock.sleep(60*10);
- 9 exit;
- 10 end loop;
- 11 end;
- 12 /
- 這段代碼會打開一個游標,對2000萬的數據量進行排序,然后在循環中只取一條數據,然后就進入sleep。在另一個窗口中監控到2111這個會話的event變成了PL/SQL lock timer,就去查詢v$sort_usage:
- select a.sql_id sort_sql_id,b.sql_id,b.prev_sql_id, contents,segtype,blocks*8/1024/1024 gb
- 2 from v$sort_usage a,v$session b
- 3 where a.session_addr=b.saddr
- 4 and b.sid=2111;
- SORT_SQL_ID SQL_ID PREV_SQL_ID CONTENTS SEGTYPE GB
- ------------- ------------- ------------- --------- --------- -----------
- fabh24prgk2sj bhzf316mdc07w fabh24prgk2sj TEMPORARY SORT 1.444824219
- 可以看到v$sort_usage中的SQL_ID(即上述結果中SORT_SQL_ID)與v$session中的pre_sql_id一致,這條SQL是:
- @sqlbyid fabh24prgk2sj
- SQL_FULLTEXT
- --------------------------------------------------------
- SELECT OBJECT_NAME FROM DBA_OBJECTS WHERE OBJECT_ID=:B1
- 而實際上當前正在執行的SQL是:
- @sqlbyid bhzf316mdc07w
- SQL_FULLTEXT
- ---------------------------------------------------------------------------
- declare
- v_object_name varchar2(100);
- v_dummy varchar2(100);
- begin
- for rec in (select * from t1 order by object_id,object_name) loop
- select object_type into v_dummy from t1 where rownum<=1;
- select object_name into v_object_name from dba_objects where object_id=rec.object_id;
- dbms_lock.sleep(60*10);
- exit;
- end loop;
- end;
問題分析到這里,很明顯確認的是,應用存在問題,也許是業務邏輯問題;也許是根據前台選擇的條件拼接的SQL,但是沒有任何條件時就查詢了所有數 據。接下來就是找來開發人員,至於后面的事就跟這個主題沒有太大關系。我們可以根據這個案例來進一步展開,去探尋臨時表空間的更多知識點。
這里要展開的第1點是,v$sort_usage中的sql_id是不是會話正在執行的SQL,我們去看看視圖fixed_View_definition就知道了:
- select x$ktsso.inst_id, username, username, ktssoses, ktssosno, prev_sql_addr, prev_hash_value,
- prev_sql_id, ktssotsn, decode(ktssocnt, 0, 'PERMANENT', 1, 'TEMPORARY'), decode(ktssosegt, 1,
- 'SORT', 2, 'HASH', 3, 'DATA', 4, 'INDEX', 5, 'LOB_DATA', 6, 'LOB_INDEX' , 'UNDEFINED'), ktssofno,
- ktssobno, ktssoexts, ktssoblks, ktssorfno from x$ktsso, v$session where ktssoses = v$session.saddr
- 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,以上述的測試結果為例,查詢這個基表的結果如下:
- 2 and ktssosno = v$session.serial#
- 3 and v$session.sid=2111;
- KTSSOSQLID
- -------------
- 60t6fmjsw6v8y
- @sqlbyid 60t6fmjsw6v8y
- SQL_FULLTEXT
- ---------------------------------------------------------------------------
- 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,使用如下的查詢來代替:
- ktssoses "SADDR",
- sid,
- ktssosno "SERIAL#",
- username "USERNAME",
- osuser "OSUSER",
- ktssosqlid "SQL_ID",
- ktssotsn "TABLESPACE",
- decode(ktssocnt, 0, 'PERMANENT', 1, 'TEMPORARY') "CONTENTS",
- --注意在12c的v$sort_usage定義中TABLESPACE和CONTENTS已經發生變化了。
- decode(ktssosegt, 1, 'SORT', 2, 'HASH', 3, 'DATA', 4, 'INDEX',
- 5, 'LOB_DATA', 6, 'LOB_INDEX' , 'UNDEFINED') "SEGTYPE",
- ktssofno "SEGFILE#",
- ktssobno "SEGBLK#",
- ktssoexts "EXTENTS",
- ktssoblks "BLOCKS",
- round(ktssoblks*p.value/1024/1024, 2) "SIZE_MB",
- ktssorfno "SEGRFNO#"
- from x$ktsso k, v$session s,
- (select value from v$parameter where name='db_block_size') p
- where ktssoses = s.saddr
- and ktssosno = s.serial#;
要展開的第2點是,v$sort_usage中的SEGTYPE列的不同的值各有什么意義:
- SORT:SQL排序使用的臨時段,包括order by、group by、union、distinct、窗口函數(window function)、建索引等產生的排序。
- DATA:臨時表(Global Temporary Table)存儲數據使有的段。
- INDEX:臨時表上建的索引使用的段。
- HASH:hash算法,如hash連接所使用的臨時段。
- LOB_DATA和LOB_INDEX:臨時LOB使用的臨時段。
根據上述的段類型,大體可以分為三類占用:
- SQL語句排序、HASH JOIN占用
- 臨時表占用
- 臨時LOB對象占用
臨時表空間的異常占用,一種緩步增長的,另一種情況:一下撐滿的通常是一個極大數據量的排序或極大的索引的創建。緩步增長的情況,跟系統的內存被逐 漸占用類似,存在“泄露”。比如排序的SQL游標沒有關閉,比如本文的案例;比如會話級臨時表產生了數據后一直沒有清除;臨時LOB對象沒有清理或泄露。 前兩種比較好去分析處理,但是臨時LOB的泄露問題就復雜很多。
來看一個測試:
- SID
- -----------
- 1773
- declare
- 2 v_lob clob;
- 3 begin
- 4 dbms_lob.createtemporary(v_lob,true);
- 5 dbms_lob.writeappend(v_lob,1000,lpad('a',1000,'a'));
- 6 end;
- 7 /
上述的代碼執行完之后,在另一個窗口中,我們查詢v$sort_usage:
- select a.sql_id sort_sql_id,b.sql_id,b.prev_sql_id, contents,segtype,blocks*8/1024/1024 gb
- 2 from v$sort_usage a,v$session b
- 3 where a.session_addr=b.saddr
- 4 and b.sid=1773;
- SORT_SQL_ID SQL_ID PREV_SQL_ID CONTENTS SEGTYPE GB
- ------------- ------------- ------------- --------- --------- -----------
- 9babjv8yq8ru3 9babjv8yq8ru3 TEMPORARY LOB_DATA .0004882813
- @sqlbyid 9babjv8yq8ru3
- SQL_FULLTEXT
- ---------------------------------------------------------------------------
- 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臨時段的泄露或臨時段沒有釋放相關的文檔。
最后,不管是什么情況導致的臨時表空間被過多占用,通常重啟應用能夠釋放掉臨時段,因為會話退出后,相對應的臨時段就會被釋放。看來,“重啟”大法在這種情況下就很有用。