ORA-01555 快照過舊,是數據庫中很常見的一個錯誤,比如當我們的事務需要使用undo來構建CR塊的時候,而此時對應的undo 已經不存在了, 這個時候就會報ORA-01555的錯誤。
ORA-01555錯誤在Oracle 8i及之前的版本最多。從9i開始的undo自動管理,至現在的10g、11g中的undo auto tuning,使得ORA-01555的錯誤越來越少。但是這個錯誤,仍然不可避免。
Oracle undo 管理 http://www.linuxidc.com/Linux/2011-09/42706.htm
1. 出現ORA-01555錯誤,通常有2種情況:
(1)SQL語句執行時間太長,或者UNDO表空間過小,或者事務量過大,或者過於頻繁的提交,導致執行SQL過程中進行一致性讀時,SQL執行後修改的前鏡像(即UNDO數據)在UNDO表空間中已經被覆蓋,不能構造一致性讀塊(CR blocks)。 這種情況最多。
(2)SQL語句執行過程中,訪問到的塊,在進行延遲塊清除時,不能確定該塊的事務提交時間與SQL執行開始時間的先後次序。 這種情況很少。
2. 第1種情況解決的辦法:
(1)增加UNDO表空間大小
(2)增加undo_retention 時間,默認只有15分鐘
(3)優化出錯的SQL,減少查詢的時間,首選方法
(4)避免頻繁的提交
有關Undo 的更多信息,參考我的文章:
Oracle undo 表空間管理 http://www.linuxidc.com/Linux/2011-07/38006.htm
3. 第二種情況描述
在塊清除過程中,如果一個塊已被修改,下一個會話訪問這個塊時,可能必須查看最後一個修改這個塊的事務是否還是活動的。一旦確定該事務不再活動,就會完成塊清除,這樣另一個會話訪問這個塊時就不必再歷經同樣的過程。
要完成塊清除,Oracle 會從塊首部確定前一個事務所用的undo 段(ITL),然後確定從undo 首部能不能看出這個塊是否已經提交。
可以用以下兩種方式完成這種確認:
一種方式是Oracle 可以確定這個事務很久以前就已經提交,它在undo 段事務表中的事務槽已經被覆蓋。
另一種情況是COMMIT SCN 還在undo 段的事務表中,這說明事務只是稍早前剛提交,其事務槽尚未被覆蓋。
當滿足以下條件時,就會從defered clean 收到ORA-01555的錯誤:
首先做了一個修改並COMMIT,塊沒有自動清理(即沒有自動完成“提交清除”,例如修改了太多的塊,在SGA 塊緩沖區緩存的10%中放不下)。
其他會話沒有接觸這些塊,而且在我們這個“倒霉”的查詢(稍後顯示)命中這些塊之前,任何會話都不會接觸它們。
開始一個長時間運行的查詢。這個查詢最後會讀其中的一些塊。這個查詢從SCN t1 開始,這就是讀一致SCN,必須將數據回滾到這一點來得到讀一致性。
開始查詢時,上述修改事務的事務條目還在undo 段的事務表中。查詢期間,系統中執行了多個提交。執行事務沒有接觸執行已修改的塊(如果確實接觸到,也就不存在問題了)。
由於出現了大量的COMMIT,undo 段中的事務表要回繞並重用事務槽(ITL)。最重要的是,將循環地重用原來修改事務的事務條目。另外,系統重用了undo 段的區段,以避免對undo 段首部塊本身的一致讀。
此外,由於提交太多,undo 段中記錄的最低SCN 現在超過了t1(高於查詢的讀一致SCN)。如果查詢到達某個塊,而這個塊在查詢開始之前已經修改並提交,就會遇到麻煩。正常情況下,會回到塊所指的undo 段,找到修改了這個塊的事務的狀態(換句話說,它會找到事務的COMMIT SCN)。
如果這個COMMIT SCN 小於t1,查詢就可以使用這個塊。如果該事務的COMMIT SCN 大於t1,查詢就必須回滾這個塊。不過,問題是,在這種特殊的情況下,查詢無法確定塊的COMMIT SCN 是大於還是小於t1。相應地,不清楚查詢能否使用這個塊映像。這就導致了ORA-01555 錯誤。
大批量的UPDATE 或INSERT 會導致塊清除(block cleanout),所以在大批量UPDATE 或大量加載之後使用DBMS_STATS收集相關對象的統計信息,加載之後完成這些對象的清理。
關於塊清除這塊,在
老熊blog上關於defered Clean的場景說明:
(1)有事務大量修改了A表的數據,或者A表的數據雖然被事務少量修改,但是一部分修改過的塊已經刷出內存並寫到了磁盤上。隨即事務提交,提交時刻為SCN1。而提交時有數據塊上的事務沒有被清除。
(2)在SCN2時刻,開始執行SELECT查詢A表,對A表進行全表掃描,而且A表很大。也可能是其他情況,比如是小表,但是是一個游標方式的處理過程,而處理過程中又非常耗時。注意,這裡SCN2與SCN1之間可能相隔了很遠,從時間上來說,甚至可能有數十天。不管怎麼樣,這在SCN1至SCN2時間之間,系統中存在大量的事務,使得UNDO表空間的塊以及UNDO段頭的事務表全部被重用過。
(3)SELECT語句在讀A表的一個塊時,發現表上有活動事務,這是由於之前的事務沒有清除所致。ORACLE根據數據塊中ITL的XID檢查事務表,這時會有2種情況:
(A)XID對應的事務表中的記錄仍然存在並發現事務已經提交,可以得到事務准確的提交SCN(commit scn),稱為SCN3,等於SCN1。很顯然,由於查詢的時刻SCN2晚於事務提交的時刻SCN1,那麼不需要構造一致性讀塊。
(B)XID對應的事務表中的記錄已經被重用,這個時候仍然表明表明事務已經被提交。那麼這個時候,Oracle沒辦法准確地知道事務的提交時間,只能記錄為這樣一個事實,事務提交的SCN小於其UNDO段的事務表中最近一次重用的事務記錄的SCN(即這個事務表最老的事務SCN)。這裡稱這個SCN為SCN4。
(4)SCN4可能遠小於SCN2,那是因為事務很早之前就已經提交。也可能SCN4大於SCN2,這是因為SELECT語句執行時間很長,同時又有大量的事務已經將事務表重用。對於後者,很顯然,Oracle會認為該事務的提交時間可能在SELECT開始執行之後。這裡為什麼說可能,是因為ORACLE只能判斷出事務是在SCN4之前提交的,並不是就剛好在SCN4提交。而此時,利用UNDO BLOCK進行一致性讀數據的構造也很可能失敗,因為UNDO BLOCK很可能已經被覆蓋,特別是SCN1遠小於SCN2的情況下。在這種情況下,ORA-01555錯誤就會出現。
對這種由於表上存在未清除的事務,同時導出時間過長,UNDO段頭的事務表被全部重用,ORACLE在查詢到有未清除事務的塊時不能確定事務提交時間是否早於導出(查詢)開始時間,報ORA-01555錯誤。
老熊blog上有2個解決方法,一是提高SQL 性能,另一個是清除表上的事務,即延時塊清楚(Defered Clean)。 這個方法也很簡單,就是select。
SQL>SELECT /*+ FULL(A) */ COUNT(*) FROM BIG_TABLE A;
SELECT COUNT(*),速度顯然大大高於SELECT *,所需的時間也更短,出現ORA-01555錯誤的可能性就非常低了。
注意:
(1)使用FULL HINT,以避免查詢進行索引快速全掃描,而不是對表進行全表掃描。
(2)這裡不能為了提高性能而使用PARALLEL(並行),測試表明,在表上進行並行查詢,以DIRECT READ方式讀取表並不會清除掉表上的事務。
如果表過大,SELECT COUNT(*)的時間過長,那麼我們可以用下面的代碼將表分成多個段,進行分段查詢。
/* Formatted on 2011/6/29 19:18:40 (QP5 v5.163.1008.3004) */ SELECT DBMS_ROWID.rowid_create (1, oid1, fid1, bid1, 0) rowid1, DBMS_ROWID.rowid_create (1, oid2, fid2, bid2, 9999) rowid2 FROM (SELECT a.*, ROWNUM rn FROM ( SELECT chunk_no, MIN (oid1) oid1, MAX (oid2) oid2, MIN (fid1) fid1, MAX (fid2) fid2, MIN (bid1) bid1, MAX (bid2) bid2 FROM (SELECT chunk_no, FIRST_VALUE ( data_object_id) OVER ( PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) oid1, LAST_VALUE ( data_object_id) OVER ( PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) oid2, FIRST_VALUE ( relative_fno) OVER ( PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) fid1, LAST_VALUE ( relative_fno) OVER ( PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) fid2, FIRST_VALUE ( block_id) OVER ( PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) bid1, LAST_VALUE ( block_id + blocks - 1) OVER ( PARTITION BY chunk_no ORDER BY data_object_id, relative_fno, block_id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) bid2 FROM (SELECT data_object_id, relative_fno, block_id, blocks, CEIL (sum2 / chunk_size) chunk_no FROM (SELECT /*+ rule */ b.data_object_id, a.relative_fno, a.block_id, a.blocks, SUM ( a.blocks) OVER ( ORDER BY b.data_object_id, a.relative_fno, a.block_id) sum2, CEIL ( SUM (a.blocks) OVER () / &trunks) chunk_size FROM dba_extents a, dba_objects b WHERE a.owner = b.owner AND a.segment_name = b.object_name AND NVL (a.partition_name, '-1') = NVL (b.subobject_name, '-1') AND b.data_object_id IS NOT NULL AND a.owner = UPPER ('&owner') AND a.segment_name = UPPER ('&table_name')))) GROUP BY chunk_no ORDER BY chunk_no) a);
該SQL 在執行時需要輸入幾個參數:
trunks: 表示把表分成的段數
owner: 表的所有者
table_name: 表名