一個電信運營商客戶的核心交易系統,臨時表空間大量被佔用,臨時表空間被撐到了600GB。這樣的問題複雜嗎?取決於很多因素,不過今天所要講的案例,並不複雜,如果我們對臨時表空間在何種情況下使用有足夠了解。
首先,我們要去檢查是什麼會話佔用了臨時表空間,具體佔用了多少,臨時段的具體型別是什麼。正如我們要想知道這個月的花費過大,去分析原因時就要去看是哪些開銷過大、開銷了多少金額、開銷的用途等。
這個步驟比較簡單,查詢v$sort_usage就可以了:
- select * from
- (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> @sqlbyid 291nk7db4bwdh
- 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:
- select sid,prev_sql_id, sql_id from v$session where saddr='070000294AC0D050';
- 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是什麼:
- @sqlbyid cfpdpb526ad43
- 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語句看看:
- COUNT(*)
- --------
- 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,以上述的測試結果為例,查詢這個基表的結果如下:
- select ktssosqlid from x$ktsso, v$session where ktssoses = v$session.saddr
- 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,使用如下的查詢來代替:
- select k.inst_id "INST_ID",
- 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的洩露問題就複雜很多。
來看一個測試:
- select sid from v$mystat where rownum<=1;
- 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_DA他的臨時段。雖然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臨時段的洩露或臨時段沒有釋放相關的文件。
最後,不管是什麼情況導致的臨時表空間被過多佔用,通常重啟應用能夠釋放掉臨時段,因為會話退出後,相對應的臨時段就會被釋放。看來,“重啟”大法在這種情況下就很有用。
--The END.