一个电信运营商客户的核心交易系统,临时表空间大量被占用,临时表空间被撑到了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