redo(重作信息)是Oracle在在線(或歸檔)重作日誌文件中記錄的信息,萬一出現失敗時能夠利用這些數據來「重放」(或重作)事務。undo(撤銷信息)是Oracle在undo段中記錄的信息,用於取消或回滾事務。java
重作日誌文件(redo log file)對Oracle數據庫來講相當重要。它們是數據庫的事務日誌。Oracle維護着兩類重作日誌文件:在線(online)重作日誌文件和歸檔(archived)重作日誌文件。這兩類重作日誌文件都用於恢復;其主要目的是,萬一實例失敗或介質失敗,它們就能派上用場。
若是數據庫所在主機掉電,致使實例失敗,Oracle會使用在線重作日誌將系統剛好恢復到掉電以前的那個時間點。若是磁盤驅動器出現故障(這是一個介質失敗),Oracle會 使用歸檔重作日誌以及在線重作日誌將該驅動器上的數據備份恢復到適當的時間點。另外,若是你「無心地」截除了一個表,或者刪除了某些重要的信息,而後提交 了這個操做,那麼能夠恢復受影響數據的一個備份,並使用在線和歸檔重作日誌文件把它恢復到這個「意外」發生前的時間點。
歸檔重作日誌文件實際上就是已填滿的「舊」在線重作日誌文件的副本。系統將日誌文件填滿時,ARCH進程會在另外一個位置創建在線重作日誌文件的一個副本,也能夠在本地和遠程位置上創建多個另外的副本。若是因爲磁盤驅動器損壞或者其餘物理故障而致使失敗,就會用這些歸檔重作日誌文件來執行介質恢復。Oracle拿到這些歸檔重作日誌文件,並把它們應用於數據文件的備份,使這些數據文件能「遇上」數據庫的其他部分。歸檔重作日誌文件是數據庫的事務歷史。sql
利用閃回技術(flashback),能夠執行閃回查詢(也就是說,查詢過去某個時間點的數據),取消數據庫表的刪除,將表置回到之前某個時間的狀態,等等。所以,如今使用備份和歸檔重作日誌文件來完成傳統恢復的狀況愈來愈少。不過,執行恢復是DBA最重要的任務,並且DBA在數據庫恢復方面絕對不能犯錯誤。數據庫
每一個Oracle數據庫都至少有兩個在線重作日誌組,每一個組中至少有一個成員(重作日誌文件)。這些在線重作日誌組以循環方式使用。Oracle會寫組1中的日誌文件,等寫到組1中文件的最後時,將切換到日誌文件組2,開始寫這個組中的文件。等到把日誌文件組2寫滿時,會再次切換回日誌文件組1(假設只有兩個重作日誌文件組;若是有3個重作日誌文件組,Oracle固然會繼續寫第3個組)。編程
數據庫之因此成爲數據庫(而不是文件系統等其餘事物),是由於它有本身獨有的一些特徵,重作日誌或事務日誌就是其中重要的特性之一。重作日誌多是數據庫中最重要的恢復結構,不過,若是沒有其餘部分(如undo段、分佈式事務恢復等),但靠重作日誌什麼也作不了。重作日誌是數據庫區別於傳統文件系統的一個主要因素。Oracle正寫到一半的時候有可能發生掉電,利用在線重作日誌,咱們就能有效地從這個掉電失敗中恢復。歸檔重作日誌則容許咱們從介質失敗中恢復,如硬盤損壞,或者因爲人爲錯誤而致使數據丟失。若是沒有重作日誌,數據庫提供id保護就比文件系統多不了多少。c#
從概念上講,undo正好與redo相對。你對數據執行修改時,數據庫會生成undo信息,這樣萬一你執行的事務或語句因爲某種緣由失敗了,或者若是你用一條ROLLBACK語句請求回滾,就能夠利用這些undo信息將數據放回到修改前的樣子。redo用於在失敗時重放事務(即恢復事務),undo則用於取消一條語句或一組語句的做用。與redo不一樣,undo在數據庫內部存儲在一組特殊的段中,這稱爲undo段(undo segment)。緩存
「回滾段」(rollback segment)和「undo段「(undo segment)通常認爲是同義詞。使用手動undo管理時,DBA會建立」回滾段「。使用自動undo管理時,系統將根據須要自動地建立和銷燬」undo段「。安全
一般對undo有一個誤解,認爲undo用 於數據庫物理地恢復到執行語句或事務以前的樣子,但實際上並不是如此。數據庫只是邏輯地恢復到原來的樣子,全部修改都被邏輯地取消,可是數據結構以及數據庫塊自己在回滾後可能大不相同。緣由在於:在全部多用戶系統中,可能會有數10、數百甚至數千個併發事務。數據庫的主要功能之一就是協調對數據的併發訪問。也許咱們的事務在修改一些塊,而通常來說每每會有許多其餘的事務也在修改這些塊。所以,不能簡單地將一個塊放回到咱們的事務開始前的樣子,這樣會撤銷其餘人 (其餘事務)的工做!性能優化
例如,假設咱們的事務執行了一個INSERT語句,這條語句致使分配一個新區段(也就是說,致使表的空間增大)。經過執行這個INSET,咱們將獲得一個新的塊,格式化這個塊以便使用,並在其中放上一些數據。此時,可能出現另外某個事務,它也向這個塊中插入數據。若是要回滾咱們的事務,顯然不能取消對這個塊的格式化和空間分配。所以,Oracle回滾時,它實際上會作與先前邏輯上相反的工做。對於每一個INSERT,Oracle會完成一個DELETE。對於每一個DELETE,Oracle會執行一個INSERT。對於每一個UPDATE,Oracle則會執行一個「反UPDATE「,或者執行另外一個UPDATE將修改前的行放回去。服務器
這種undo生成對於直接路徑操做(direct path operation)不適用,直接路徑操做可以繞過表上的undo生成。
怎麼才能看到undo生成(undo generation)具體是怎樣的呢?也許最容易的方法就是遵循如下步驟:
(1) 建立一個空表。
(2) 對它作一個所有掃描,觀察讀表所執行的I/O數量。
(3) 在表中填入許多行(但沒有提交)
(4) 回滾這個工做,並撤銷。
(5) 再次進行全表掃描,觀察所執行的I/O數量。session
scott@ORCL>create table t 2 as 3 select * 4 from all_objects 5 where 1=0; 表已建立。
而後查詢這個表,這裏在SQL*Plus中啓用了AUTOTRACE,以便能測試I/O。
在這個例子中,每次(即每一個用例)都會作兩次全表掃描。咱們的目標只是測試每一個用例中第二次完成的I/O。這樣能夠避免統計在解析和優化期間優化器可能完成的額外I/O。
最初,這個查詢須要0個I/O來完成這個表的全表掃描:
scott@ORCL>select * from t; 未選定行 scott@ORCL>set autotrace traceonly statistics scott@ORCL>select * from t; 未選定行 統計信息 ---------------------------------------------------------- 0 recursive calls 0 db block gets 0 consistent gets 0 physical reads 0 redo size 1344 bytes sent via SQL*Net to client 509 bytes received via SQL*Net from client 1 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 0 rows processed scott@ORCL>set autotrace off
接下來,向表中增長大量數據。這會使它「擴大「,不過隨後再將其回滾:
scott@ORCL>insert into t select * from all_objects; 已建立71902行。 scott@ORCL>rollback; 回退已完成。
如今,若是再次查詢這個表,會發現這一次讀表所需的I/O比先前多得多:
scott@ORCL>select * from t; 未選定行 scott@ORCL>set autotrace traceonly statistics scott@ORCL>select * from t; 未選定行 統計信息 ---------------------------------------------------------- 0 recursive calls 0 db block gets 1086 consistent gets 0 physical reads 0 redo size 1344 bytes sent via SQL*Net to client 509 bytes received via SQL*Net from client 1 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 0 rows processed
前面的INSERT致使將一些塊增長到表的高水位線(high-water mark,HWM)之下,這些塊沒有由於回滾而消失,它們還在那裏,並且已經格式化,只不過如今爲空。全表掃描必須讀取這些塊,看看其中是否包含行。這說明,回滾只是一個「將數據庫還原「的邏輯操做。數據庫並不是真的還原成原來的樣子,只是邏輯上相同而已。
儘管undo信息存儲在undo表空間或undo段中,但也會受到redo的保護。換句話說,會把undo數據當成是表數據或索引數據同樣,對undo的修改會生成一些redo,這些redo將計入日誌。爲何會這樣呢?稍後在討論系統崩潰時發生的狀況時將會解釋它,到時你會明白了。將undo數據增長到undo段中,並像其餘部分的數據同樣在緩衝區緩存中獲得緩存。
做爲一個例子,咱們將分析對於下面這組語句可能發生什麼狀況:
insert into t (x,y) values (1,1); update t set x = x+1 where x = 1; delete from t where x = 2;
咱們會沿着不一樣的路徑完成這個事務,從而獲得如下問題的答案:
若是系統在處理這些語句的不一樣時間點上失敗,會發生什麼狀況?
若是在某個時間點上ROLLBACK,會發生什麼狀況?
若是成功並COMMIT,會發生什麼狀況?
對於第一條INSERT INTO T語句,redo和undo都會生成。所生成的undo信息足以使INSERT「消失「。INSERT INTO T生成的redo信息則足以讓這個插入」再次發生「。
這裏緩存了一些已修改的undo塊、索引塊和表數據塊。這些塊獲得重作日誌緩衝區中相應條目的「保護「。
假想場景:系統如今崩潰
SGA會被清空,可是咱們並不須要SGA裏的任何內容。重啓動時就好像這個事務歷來沒有發生過同樣。沒有將任何已修改的塊刷新輸出到磁盤,也沒有任何redo刷新輸出到磁盤。咱們不須要這些undo或redo信息來實現實例失敗恢復。
假想場景:緩衝區緩存如今已滿
在這種狀況下,DBWR必須留出空間,要把已修改的塊從緩存刷新輸出。若是是這樣,DBWR首先要求LGWR將保護這些數據庫塊的redo條目刷新輸出。DBWR將任何有修改的塊寫至磁盤以前,LGWR必須先刷新輸出與這些塊相關的redo信息。這是有道理的——若是咱們要刷新輸出表T中已修改的塊,但沒有刷新輸出與undo塊關聯的redo條目,假若系統失敗了,此時就會有一個已修改的表T塊,而沒有與之相關的redo信息。在寫出這些塊以前須要先刷新輸出重作日誌緩存區,這樣就能重作(重作)全部必要的修改,將SGA放回到如今的狀態,從而能發生回滾。
咱們生成了一些已修改的表和索引塊。這些塊有一些與之關聯的undo段塊,這3類塊都會生成redo來保護本身。重作日誌緩衝區 會在如下狀況刷新輸出:每3秒一次;緩衝區1/3滿時或者包含了1MB的緩衝數據;或者是隻要發生提交就會刷新輸出。重作日誌緩衝區還有可能會在處理期間的某一點上刷新輸出。
UPDATE所帶來的工做與INSERT大致同樣。不過UPDATE生成的undo量更大;因爲存在更新,因此須要保存一些「前「映像。
塊緩衝區緩存中會有更多新的undo段塊。爲了撤銷這個更新,若是必要,已修改的數據庫表和索引塊也會放在緩存中。咱們還生成了更多的重作日誌緩存區條目。下面假設前面的插入語句生成了一些重作日誌,其中有些重作日誌已經刷新輸出到磁盤上,有些還放在緩存中。
假想場景:系統如今崩潰
啓動時,Oracle會讀取重作日誌,發現針對這個事務的一些重作日誌條目。給定系統的當前狀態,利用重作日誌文件中對應插入的redo條目,並利用仍在緩衝區中對應插入的redo信息,Oracle會「前滾」插入。如今有一些undo塊(用以撤銷插入)、已修改的表塊(剛插入後的狀態),以及已修改的索引塊(剛插入後的狀態)。因爲系統正在進行崩潰恢復,並且咱們的會話還再也不鏈接(這是固然),Oracle發現這個事務從未提交,所以會將其回滾。它取剛剛在緩衝區緩存中前滾獲得的undo,並將這些undo應用到數據和索引塊,使數據和索引塊「恢復」爲插入發生前的樣子。如今一切都回到從前。磁盤上的塊可能會反映前面的INSERT,也可能不反映(這取決於在崩潰前是否已經將塊刷新輸出)。若是磁盤上的塊確實反映了插入,而實際上如今插入已經被撤銷,當從緩衝區緩存刷新輸出塊時,數據文件就會反映出插入已撤銷。若是磁盤上的塊原本就沒有反映前面的插入,就不用去管它——這些塊之後確定會被覆蓋。
這個場景涵蓋了崩潰恢復的基本細節。系統將其做爲一個兩步的過程來完成。首先前滾,把系統放到失敗點上,而後回滾還沒有提交的全部工做。這個動做會再次同步數據文件。它會重放已經進行的工做,並撤銷還沒有完成的全部工做。
假想場景:應用回滾事務
此時,Oracle會發現這個事務的undo信息可能在緩存的undo段塊中(基本上是這樣),也可能已經刷新輸出到磁盤上(對於很是大的事務,就每每是這種狀況)。它會把undo信息應用到緩衝區緩存中的數據和索引塊上,或者假若數據和索引塊已經不在緩存中,則要從磁盤將數據和索引塊讀入緩存,再對其應用undo。這些塊會恢復爲其原來的行值,並刷新輸出到數據文件。
這個場景比系統崩潰更常見。須要指出,有一點頗有用:回滾過程當中從不涉及重作日誌。只有恢復和歸檔時會當前重作日誌。這對於調優是一個很重要的概念:重作日誌是用來寫的(而不是用於讀)。Oracle不會在正常的處理中讀取重作日誌。只要你有足夠的設備,使得ARCH讀文件時,LGWR能寫到另外一個不一樣的設備,那麼就不存在重作日誌競爭。Oracle的目標是:能夠順序地寫日誌,並且在寫日誌時別人不會讀日誌。
一樣,DELETE會生成undo,塊將被修改,並把redo發送到重作日誌緩衝區。這與前面沒有太大的不一樣。實際上,它與UPDATE如此相似。
咱們已經看到了多種失敗場景和不一樣的路徑,如今終於到COMMIT了。在此,Oracle會把重作日誌緩衝區刷新輸出到磁盤。
已修改的塊放在緩衝區緩存中;可能有一些塊已經刷新輸出到磁盤上。重作這個事務所需的所有redo都安全地存放在磁盤上,如今修改已是永久的了。若是從數據文件直接讀取數據,可能會看到塊仍是事務發生前的樣子,由於頗有可能DBWR尚未(從緩衝區緩存)寫出這些塊。這沒有關係,若是出現失敗,能夠利用重作日誌文件來獲得最新的塊。undo信息會一直存在,除非undo段迴繞重用這些undo塊。若是某些對象受到影響,Oracle會使用這個undo信息爲須要這些對象的會話提供對象的一致讀。
COMMIT一般是一個很是快的操做,而不論事務大小如何。
不論事務有多大,COMMIT的響應時間通常都很「平」(flat,能夠理解爲無高低變化)。這是由於COMMIT並無太多的工做去作,不過它所作的確實相當重要。
這 一點很重要,之因此要了解並掌握這個事實,緣由之一是:這樣你就能心無芥蒂地讓事務有足夠的大小。許多開發人員會人爲地限制事務的大 小,分別提交太多的行,而不是一個邏輯工做單元完成後才提交。這樣作主要是出於一種錯誤的信念,即認爲能夠節省稀有的系統資源,而實際上這只是增長了資源 的使用。若是一行的COMMIT須要X個時間單位,1,000次COMMIT也一樣須要X個時間單位,假若採用如下方式執行工做,即每行提交一次共執行1,000次COMMIT,就會須要1000*X各時間單位才能完成。若是隻在必要時才提交(即邏輯工做單元結束時),不只能提升性能,還能減小對共享資源的競爭(日誌文件、各類內部閂等)。經過一個簡單的例子就能展現出過多的提交要花費更長的時間。這裏將使用一個Java應用,不過對於大多數其餘客戶程序來講,結果可能都是相似的,只有PL/SQL除外。首先,下面是咱們要插入的示例表:
scott@ORCL>create table test 2 ( 3 ID NUMBER not null, 4 CODE VARCHAR2(20), 5 DESCR VARCHAR2(20), 6 INSERT_USER VARCHAR2(30) , 7 INSERT_DATE DATE 8 ); 表已建立。 scott@ORCL>desc test 名稱 是否爲空? 類型 ------------- -------- ------------------------ ID NOT NULL NUMBER CODE VARCHAR2(20) DESCR VARCHAR2(20) INSERT_USER VARCHAR2(30) INSERT_DATE DATE
Java程序要接受兩個輸入:要插入(INSERT)的行數(iters),以及兩次提交之間插入的行數(commitCnt)。它先鏈接到數據庫,將autocommit(自動提交)設置爲off(全部Java代碼都應該這麼作),而後將doInserts()方法共調用3次:
第一次調用只是「熱身」(確保全部類都已經加載)。
第二次調用指定了要插入(INSERT)的行數,並指定一次提交多少行(即每N行提交一次)。
最後一次調用將要插入的行數和一次提交的行數設置爲相同的值(也就是說,全部行都插入以後才提交)。
而後關閉鏈接,並退出:
import corp.creditease.rsc.service.IDataCarrierService; import corp.creditease.rsc.service.impl.DataCarrierServiceImpl; import org.junit.Test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Date; /** * Created by Anna on 2017/7/21. */ public class Test1 { public static void main(String arr[]) throws Exception { try { DriverManager.registerDriver(new oracle.jdbc.OracleDriver()); Connection con = DriverManager.getConnection ("jdbc:oracle:thin:@localhost:1521:orcl", "scott", "123456"); Integer iters = new Integer(arr[0]); Integer commitCnt = new Integer(arr[1]); con.setAutoCommit(false); doInserts(con, 1, 1); doInserts(con, iters.intValue(), commitCnt.intValue()); doInserts(con, iters.intValue(), iters.intValue()); con.commit(); con.close(); } catch (SQLException e) { e.printStackTrace(); } } static void doInserts(Connection con, int count, int commitCount) throws Exception { PreparedStatement ps = con.prepareStatement ("insert into test " + "(id, code, descr, insert_user, insert_date)" + " values (?,?,?, user, sysdate)"); int rowcnt = 0; int committed = 0; long start = new Date().getTime(); for (int i = 0; i < count; i++) { ps.setInt(1, i); ps.setString(2, "PS - code" + i); ps.setString(3, "PS - desc" + i); ps.executeUpdate(); rowcnt++; if (rowcnt == commitCount) { con.commit(); rowcnt = 0; committed++; } } con.commit(); long end = new Date().getTime(); System.out.println ("pstatement " + count + " times in " + (end - start) + " milli seconds committed = " + committed); } }
doInserts()方法至關簡單。首先準備(解析)一條INSERT語句,以便屢次反覆綁定/執行這個INSERT。
而後根據要插入的行數循環,反覆綁定和執行這個INSERT。另外,它會檢查一個行計數器,查看是否須要COMMIT,或者是否已經不在循環範圍內。還要注意,咱們分別在循環以前和循環以後獲取了當前時間,從而監視並報告耗用的時間。
下面根據不一樣的輸入發放運行這個代碼:
pstatement 1 times in 0 milli seconds committed = 1 pstatement 10000 times in 5501 milli seconds committed = 10000 pstatement 10000 times in 1961 milli seconds committed = 1
----------------------------------------------------------------------------------------
pstatement 1 times in 0 milli seconds committed = 1 pstatement 100000 times in 20721 milli seconds committed = 10000 pstatement 100000 times in 9157 milli seconds committed = 1
----------------------------------------------------------------------------------------
pstatement 1 times in 31 milli seconds committed = 1 pstatement 1000000 times in 113189 milli seconds committed = 10000 pstatement 1000000 times in 95054 milli seconds committed = 1
能夠看到,提交得越多,花費的時間就越長。這只是單用戶的狀況,若是有多個用戶在作一樣的工做,全部這些用戶都過於頻繁地提交,那麼獲得的數字將飛速增加。
在其餘相似的狀況下,咱們也不止一次地聽到過一樣的「故事」。例如,若是不使用綁定變量,並且頻繁地完成硬解析,這會嚴重地下降併發性,緣由是存在庫緩存競爭和過量的CPU佔用。即便轉而使用綁定變量,若是過於頻繁地軟解析,也會帶來大量的開銷(致使過多軟解析的緣由多是:執意地關閉遊標,儘管稍後就會重用這些遊標)。必須在必要時才完成操做,COMMIT就是這樣的一種操做。最好根據業務需求來肯定事務的大小,而不是錯誤地爲了減小數據庫上的資源使用而「壓縮」事務。
在這個例子中,COMMIT的開銷存在兩個因素:
顯然會增長與數據庫的往返通訊。若是每一個記錄都提交,生成的往返通訊量就會大得多。
每次提交時,必須等待redo寫至磁盤。這會致使「等待」。在這種狀況下,等待稱爲「日誌文件同步」(log file sync)。
只需對這個Java應用稍作修改就能夠觀察到後面這一條。咱們將作兩件事情:
增長一個DBMS_MONITOR調用,啓用對等待事件的SQL跟蹤。在Oracle9i中,則要使用alter session set events ‘10046 trace name context forever, level 12’,由於
DBMS_MONITOR是Oracle 10g中新增的。
把con.commit()調用改成一條完成提交的SQL語句調用。若是使用內置的JDBC commit()調用,這不會向跟蹤文件發出SQL COMMIT語句,而TKPROF(用於格式化跟蹤文件的工具)也不會報告完成COMMIT所花費的時間。
若是在每一個INSERT以後都提交,幾乎每次都要等待。儘管每次只等待很短的時間,可是因爲常常要等待,這些時間就會累積起來。與之造成鮮明對比,若是隻提交一次,就不會等待很長時間(實際上,這個時間實在過短了,以致於簡直沒法度量)。這說明,COMMIT是一個很快的操做;咱們但願響應時間或多或少是「平」的,而不是所完成工做量的一個函數。
那麼,爲何COMMIT的響應時間至關「平」,而不論事務大小呢?在數據庫中執行COMMIT以前,困難的工做都已經作了。咱們已經修改了數據庫中的數據,因此99.9%的工做都已經完成。例如,已經發生了如下操做:
已經在SGA中生成了undo塊。
已經在SGA中生成了已修改數據塊。
已經在SGA中生成了對於前兩項的緩存redo。
取決於前三項的大小,以及這些工做花費的時間,前面的每一個數據(或某些數據)可能已經刷新輸出到磁盤。
已經獲得了所需的所有鎖。
執行COMMIT時,餘下的工做只是:
1. 爲事務生成一個SCN。SCN是Oracle使用的一種簡單的計時機制,用於保證事務的順序,並支持失敗恢復。SCN還用於保證數據庫中的讀一致性和檢查點。能夠把SCN看做一個鐘擺,每次有人COMMIT時,SCN都會增1.
2. LGWR將全部餘下的緩存重作日誌條目寫到磁盤,並把SCN記錄到在線重作日誌文件中。這一步就是真正的COMMIT。若是出現了這一步,即已經提交。事務條目會從V$TRANSACTION中「刪除」,這說明咱們已經提交。
3. V$LOCK中記錄着咱們的會話持有的鎖,這些所都將被釋放,而排隊等待這些鎖的每個人都會被喚醒,能夠繼續完成他們的工做。
4. 若是事務修改的某些塊還在緩衝區緩存中,則會以一種快速的模式訪問並「清理」。塊清除(Block cleanout)是指清除存儲在數據庫塊首部的與鎖相關的信息。實質上講,咱們在清除塊上的事務信息,這樣下一個訪問這個塊的人就不用再這麼作了。咱們採用一種無需生成重作日誌信息的方式來完成塊清除,這樣能夠省去之後的大量工做。
能夠看到,處理COMMIT所要作的工做不多。其中耗時最長的操做要算LGWR執行的活動(通常是這樣),由於這些磁盤寫是物理磁盤I/O。不過,這裏LGWR花費的時間並不會太多,之因此能大幅減小這個操做的時間,緣由是LGWR一直在以連續的方式刷新輸出重作日誌緩衝區的內容。在你工做期間,LGWR並不是緩存着你作的全部工做;實際上,隨着你的工做的進行,LGWR會在後臺增量式地刷新輸出重作日誌緩衝區的內容。這樣作是爲了不COMMIT等待很長時間來一次性刷新輸出全部的redo。
所以,即便咱們有一個長時間運行的事務,但在提交以前,它生成的許多緩存重作日誌已經刷新輸出到磁盤了(而不是所有等到提交時才刷新輸出)。這也有很差的一面,COMMIT時,咱們必須等待,直到還沒有寫出的全部緩存redo都已經安全寫到磁盤上才行。也就是說,對LGWR的調用是一個同步(synchronous)調用。儘管LGWR自己能夠使用異步I/O並行地寫至日誌文件,可是咱們的事務會一直等待LGWR完成全部寫操做,並收到數據都已在磁盤上的確認纔會返回。
PL/SQL提供了提交時優化(commit-time optimization)。LGWR是一個同步調用,咱們要等待它完成全部寫操做。PL/SQL引擎不一樣,要認識到直到PL/SQL例程完成以前,客戶並不知道這個PL/SQL例程中是否發生了COMMIT,因此PL/SQL引擎完成的是異步提交。它不會等待LGWR完成;相反,PL/SQL引擎會從COMMIT調用當即返回。不過,等到PL/SQL例程完成,咱們從數據庫返回客戶時,PL/SQL例程則要等待LGWR完成全部還沒有完成的COMMIT。所以,若是在PL/SQL中提交了100次,而後返回客戶,會發現因爲存在這種優化,你只會等待LGWR一次,而不是100次。指導原則是,應該在邏輯工做單元完成時才提交,而不要在此以前草率地提交。
若是你在執行分佈式事務或者以最大可能性模式執行Data Guard,PL/SQL中的這種提交時優化可能會被掛起。由於此時存在兩個參與者,PL/SQL必須等待提交確實完成後才能繼續。
爲了說明COMMIT是一個「響應時間很平」的操做,下面將生成不一樣大小的redo,並測試插入(INSERT)和提交(COMMIT)的時間。爲此,仍是在SQL*Plus中使用AUTOTRACE。首先建立一個大表(要把其中的測試數據插入到另外一個表中),再建立一個空表:
scott@ORCL>@D:\app\Administrator\product\11.2.0\big_table.sql 100000 表已建立。 表已更改。 原值 3: l_rows number := &1; 新值 3: l_rows number := 100000; PL/SQL 過程已成功完成。 表已更改。 PL/SQL 過程已成功完成。 COUNT(*) ---------- 100000 scott@ORCL>create table t as select * from big_table where 1=0; 表已建立。
而後在SQL*Plus中運行如下命令:
scott@ORCL>set timing on scott@ORCL>set autotrace on statistics; scott@ORCL>insert into t select * from big_table where rownum <= 10; 已建立10行。 已用時間: 00: 00: 00.18 統計信息 ---------------------------------------------------------- 459 recursive calls 56 db block gets 72 consistent gets 6 physical reads 7196 redo size 1134 bytes sent via SQL*Net to client 1301 bytes received via SQL*Net from client 4 SQL*Net roundtrips to/from client 2 sorts (memory) 0 sorts (disk) 10 rows processed scott@ORCL>commit; 提交完成。 已用時間: 00: 00: 00.03 scott@ORCL>insert into t select * from big_table where rownum <= 100; 已建立100行。 已用時間: 00: 00: 00.03 統計信息 ---------------------------------------------------------- 1 recursive calls 13 db block gets 8 consistent gets 0 physical reads 9936 redo size 1134 bytes sent via SQL*Net to client 1302 bytes received via SQL*Net from client 4 SQL*Net roundtrips to/from client 1 sorts (memory) 0 sorts (disk) 100 rows processed scott@ORCL>commit; 提交完成。 已用時間: 00: 00: 00.00 scott@ORCL>insert into t select * from big_table where rownum <= 1000; 已建立1000行。 已用時間: 00: 00: 00.04 統計信息 ---------------------------------------------------------- 65 recursive calls 208 db block gets 66 consistent gets 15 physical reads 113008 redo size 1134 bytes sent via SQL*Net to client 1303 bytes received via SQL*Net from client 4 SQL*Net roundtrips to/from client 1 sorts (memory) 0 sorts (disk) 1000 rows processed scott@ORCL>commit; 提交完成。 已用時間: 00: 00: 00.00 scott@ORCL>insert into t select * from big_table where rownum <= 10000; 已建立10000行。 已用時間: 00: 00: 00.07 統計信息 ---------------------------------------------------------- 449 recursive calls 1807 db block gets 523 consistent gets 128 physical reads 1136052 redo size 1134 bytes sent via SQL*Net to client 1304 bytes received via SQL*Net from client 4 SQL*Net roundtrips to/from client 1 sorts (memory) 0 sorts (disk) 10000 rows processed scott@ORCL>commit; 提交完成。 已用時間: 00: 00: 00.00 scott@ORCL>insert into t select * from big_table where rownum <= 100000; 已建立100000行。 已用時間: 00: 00: 04.73 統計信息 ---------------------------------------------------------- 353 recursive calls 13306 db block gets 4315 consistent gets 1283 physical reads 12054296 redo size 1135 bytes sent via SQL*Net to client 1305 bytes received via SQL*Net from client 4 SQL*Net roundtrips to/from client 1 sorts (memory) 0 sorts (disk) 100000 rows processed scott@ORCL>commit; 提交完成。 已用時間: 00: 00: 00.00
在此監視AUTOTRACE提供的redo size(redo大小)統計,並經過set timing on監視計時信息。
嘗試插入不一樣數目的行(行數從10到100,000,每次增長一個數量級)。
插入行數 插入時間(秒) redo大小(字節) 提交時間(秒)
10 00.18 7196 00.03
100 00.03 9936 00.00
1,000 00.04 113008 00.00
10,000 00.07 1136052 00.00
100,000 04.73 12054296 00.00
能夠看到,使用一個精確度爲百分之一秒的計數器度量時,隨着生成不一樣數量的redo(從7196字節到12MB),卻幾乎測不出COMMIT時間的差別。在咱們處理和生成重作日誌時,LGWR也沒有閒着,它在後臺不斷地將緩存的重作信息刷新輸出到磁盤上。因此,咱們生成12MB的重作日誌信息時,LGWR一直在忙着,可能每1MB左右刷新輸出一次。等到COMMIT時,剩下的重作日誌信息(即還沒有寫出到磁盤的redo)已經很少了,可能與建立10行數據生成的重作日誌信息相差無幾。不論生成了多少redo,結果應該是相似的(但可能不徹底同樣)。
把COMMIT改成ROLLBACK,可能會獲得徹底不一樣的結果。回滾時間絕對是所修改數據量的一個函數。修改上一節中的腳本,要求完成一個ROLLBACK(只需把COMMIT改爲ROLLBACK),計時信息將徹底不一樣。
ROLLBACK必須物理地撤銷咱們所作的工做。相似於COMMIT,必須完成一系列操做。在到達ROLLBACK以前,數據庫已經作了大量的工做。可能已經發生的操做以下:
1. 已經在SGA中生成了undo塊。
2. 已經在SGA中生成了已修改數據塊。
3. 已經在SGA中生成了對於前兩項的緩存redo。
4. 取決於前三項的大小,以及這些工做花費的時間,前面的每一個數據(或某些數據)可能已經刷新輸出到磁盤。
5. 已經獲得了所需的所有鎖。
ROLLBACK時,要作如下工做:
1. 撤銷已作的全部修改。其完成方式以下:從undo段讀回數據,而後實際上逆向執行前面所作的操做,並將undo條目標記爲已用。若是先前插入了一行,ROLLBACK會將其刪除。若是更新了一行,回滾就會取消更新。若是刪除了一行,回滾將把它再次插入。
2. 會話持有的全部鎖都將釋放,若是有人在排隊等待咱們持有的鎖,就會被喚醒。
與此不一樣,COMMIT只是將重作日誌緩衝區中剩餘的數據刷新到磁盤。與ROLLBACK相比,COMMIT完成的工做很是少。這裏的關鍵是,除非不得已,不然不會但願回滾。回滾操做的開銷很大,由於你花了大量的時間作工做,還要花大量的時間撤銷這些工做。除非你有把握確定會COMMIT你的工做,不然乾脆什麼也別作。
做爲一名開發人員,應該可以測量你的操做生成了多少redo,這每每很重要。生成的redo越多,你的操做花費的時間就越長,整個系統也會越慢。你不光在影響你本身的會話,還會影響每個會話。redo管理是數據庫中的一個串行點。任何Oracle實例都只有一個LGWR,最終全部事務都會歸於LGWR,要求這個進程管理它們的redo,並COMMIT其事務,LGWR要作的越多,系統就會越慢。經過查看一個操做會生成多少redo,並對一個問題的多種解決方法進行測試,能夠從中找出最佳的方法。
要查看生成的redo量至關簡單,能夠使用了SQL*Plus的內置特性AUTOTRACE。不過AUTOTRACE只能用於簡單的DML,對其餘操做就力所不能及了,例如,它沒法查看一個存儲過程調用作了什麼。爲此,咱們須要訪問兩個動態性能視圖:
1. V$MYSTAT,其中有會話的提交信息。
2. V$STATNAME,這個視圖能告訴咱們V$MYSTAT中的每一行表示什麼(所查看的統計名)。
由於我常常要作這種測量,因此使用了兩個腳本,分別爲mystat和mystat2。mystat.sql腳本把我感興趣的統計初始值(如redo大小)保存在一個SQL*Plus變量中。
mystat.sql:
set verify off column value new_val V define S="&1" set autotrace off select a.name, b.value from v$statname a, v$mystat b where a.statistic# = b.statistic# and lower(a.name) like '%' || lower('&S')||'%' /
mystat2.sql腳本只是打印出該統計的初始值和結束值之差:
set verify off select a.name, b.value V, to_char(b.value-&V,'999,999,999,999') diff from v$statname a, v$mystat b where a.statistic# = b.statistic# and lower(a.name) like '%' || lower('&S')||'%' /
下面,能夠測量一個給定事務會生成多少redo。咱們只需這樣作:
@mystat "redo size" ...process... @mystat2
例如:
scott@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size" NAME VALUE ---------------------------------------------------------------- ---------- redo size 28053348 redo size for lost write detection 0 redo size for direct writes 7576 已用時間: 00: 00: 00.12 scott@ORCL>insert into t select * from big_table; 已建立100000行。 已用時間: 00: 00: 00.97 scott@ORCL>@D:\app\Administrator\product\11.2.0\mystat2 NAME V DIFF ---------------------------------------------------------------- ---------- ---------------- redo size 39946884 39,939,308 redo size for lost write detection 0 -7,576 redo size for direct writes 7576 0 已用時間: 00: 00: 00.01
如上所示,這個INSERT生成了大約39MB的redo。你可能想與一個直接路徑INSERT生成的redo作個比較,以下:
scott@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size" NAME VALUE ---------------------------------------------------------------- ---------- redo size 39946884 redo size for lost write detection 0 redo size for direct writes 7576 已用時間: 00: 00: 00.01 scott@ORCL>insert /*+ APPEND */ into t select * from big_table; 已建立100000行。 已用時間: 00: 00: 00.45 scott@ORCL>@D:\app\Administrator\product\11.2.0\mystat2 NAME V DIFF ---------------------------------------------------------------- ---------- ---------------- redo size 39980616 39,973,040 redo size for lost write detection 0 -7,576 redo size for direct writes 10072 2,496 已用時間: 00: 00: 00.00 scott@ORCL>set echo off
BEFORE觸發器要額外的redo信息,即便它根本沒有修改行中的任何值。
1. BEFORE或AFTER觸發器不影響DELETE生成的redo。
2. 在Oracle9i Release 2 及之前版本中,BEFORE或AFTER觸發器會使INSERT生成一樣數量的額外redo。在Oracle 10g中,則不會生成任何額外的redo。
3.在Oracle9i Release 2及之前的全部版本中,UPDATE生成的redo只受BEFORE觸發器的影響。AFTER觸發器不會增長任何額外的redo。不過,在Oracle 10g中,狀況又有所變化。具體表現爲:
3.1 總的來說,若是一個表沒有觸發器,對其更新期間生成的redo量老是比Oracle9i及之前版本中要少。看來這是Oracle着力解決的一個關鍵問題:對於觸發器的表,要減小這種表更新所生成的redo量。
3.2 在Oracle 10g中,若是表有一個BEFORE觸發器,則其更新期間生成的redo量比9i中更大。
3.3 若是表有AFTER觸發器,則更新所生成的redo量與9i中同樣。
爲了完成這個測試,咱們要使用一個表T,定義以下:
create table t ( x int, y char(N), z date );
可是,建立時N的大小是可變的。在這個例子中,將使用N=30、100、500、1,000和2,000來獲得不一樣寬度的行。針對不一樣大小的Y列運行測試,再來分析結果。使用了一個很小的日誌表來保存屢次運行的結果:
scott@ORCL>create table log ( what varchar2(15), -- will be no trigger, after or before 2 op varchar2(10), -- will be insert/update or delete 3 rowsize int, -- will be the size of Y 4 redo_size int, -- will be the redo generated 5 rowcnt int ) -- will be the count of rows affected 6 ; 表已建立。
這裏使用如下DO_WORK存儲過程來生成事務,並記錄所生成的redo。子過程REPORT是一個本地過程(只在DO_WORK過程當中可見),它只是在屏幕上報告發生了什麼,並把結果保存到LOG表中:
scott@ORCL>create or replace procedure do_work( p_what in varchar2 ) 2 as 3 l_redo_size number; 4 l_cnt number := 200; 5 6 procedure report( l_op in varchar2 ) 7 is 8 begin 9 select v$mystat.value-l_redo_size 10 into l_redo_size 11 from v$mystat, v$statname 12 where v$mystat.statistic# = v$statname.statistic# 13 and v$statname.name = 'redo size'; 14 15 dbms_output.put_line(l_op || ' redo size = ' || l_redo_size || 16 ' rows = ' || l_cnt || ' ' || 17 to_char(l_redo_size/l_cnt,'99,999.9') || 18 ' bytes/row' ); 19 insert into log 20 select p_what, l_op, data_length, l_redo_size, l_cnt 21 from user_tab_columns 22 where table_name = 'T' 23 and column_name = 'Y'; 24 end; 25 26 procedure set_redo_size 27 as 28 begin 29 select v$mystat.value 30 into l_redo_size 31 from v$mystat, v$statname 32 where v$mystat.statistic# = v$statname.statistic# 33 and v$statname.name = 'redo size'; 34 end; 35 36 begin 37 set_redo_size; 38 insert into t 39 select object_id, object_name, created 40 from all_objects 41 where rownum <= l_cnt; 42 l_cnt := sql%rowcount; 43 commit; 44 report('insert'); 45 46 set_redo_size; 47 update t set y=lower(y); 48 l_cnt := sql%rowcount; 49 commit; 50 report('update'); 51 52 set_redo_size; 53 delete from t; 54 l_cnt := sql%rowcount; 55 commit; 56 report('delete'); 57 end; 58 / 過程已建立。
下面將Y列的寬度設置爲2,000,而後運行如下腳原本測試3種場景:沒有觸發器、有BEFORE觸發器,以及有AFTER觸發器。
scott@ORCL>alter table t modify y char(2000); 表已更改。 scott@ORCL>exec do_work('no trigger'); insert redo size = 474996 rows = 200 2,375.0 bytes/row update redo size = 1593920 rows = 200 7,969.6 bytes/row delete redo size = 472872 rows = 200 2,364.4 bytes/row PL/SQL 過程已成功完成。 scott@ORCL>create or replace trigger before_insert_update_delete 2 before insert or update or delete on T for each row 3 begin 4 null; 5 end; 6 / 觸發器已建立 scott@ORCL>truncate table t; 表被截斷。 scott@ORCL>exec do_work('before trigger'); insert redo size = 470580 rows = 200 2,352.9 bytes/row update redo size = 894620 rows = 200 4,473.1 bytes/row delete redo size = 472528 rows = 200 2,362.6 bytes/row PL/SQL 過程已成功完成。 scott@ORCL>drop trigger before_insert_update_delete; 觸發器已刪除。 scott@ORCL>create or replace trigger after_insert_update_delete 2 after insert or update or delete on T 3 for each row 4 begin 5 null; 6 end; 7 / 觸發器已建立 scott@ORCL>truncate table t; 表被截斷。 scott@ORCL>exec do_work( 'after trigger' ); insert redo size = 501564 rows = 200 2,507.8 bytes/row update redo size = 854880 rows = 200 4,274.4 bytes/row delete redo size = 472836 rows = 200 2,364.2 bytes/row PL/SQL 過程已成功完成。
前面的輸出是在把Y大小設置爲2,000字節時運行腳本所獲得的。完成全部運行後,就能查詢LOG表,並看到如下結果:
scott@ORCL>break on op skip 1 scott@ORCL>set numformat 999,999 scott@ORCL>select op, rowsize, no_trig, 2 before_trig-no_trig, after_trig-no_trig 3 from 4 ( select op, rowsize, 5 sum(decode( what, 'no trigger', redo_size/rowcnt,0 ) ) no_trig, 6 sum(decode( what, 'before trigger', redo_size/rowcnt, 0 ) ) before_trig, 7 sum(decode( what, 'after trigger', redo_size/rowcnt, 0 ) ) after_trig 8 from log 9 group by op, rowsize 10 ) 11 order by op, rowsize 12 / OP ROWSIZE NO_TRIG BEFORE_TRIG-NO_TRIG AFTER_TRIG-NO_TRIG ---------- -------- -------- ------------------- ------------------ delete 2,000 2,364 -2 -0 insert 2,000 2,375 -22 133 update 2,000 7,970 -3,497 -3,695
日誌模式(ARCHIVELOG和NOARCHIVELOG模式)不會影響這些結果,這兩種模式獲得的結果數同樣。
觸發器對redo生成的影響
DML操做 AFTER觸發器 BEFORE觸發器
(10g) (10g)
DELETE 不影響 不影響
INSERT 常量redo 常量redo
UPDATE 增長redo 增長redo
如今你應該知道怎麼來估計redo量:
1. 估計你的「事務」大小(你要修改多少數據)。
2. 在要修改的數據量基礎上再加10%~20%的開銷,具體增長多大的開銷取決於你要修改的行數。修改行越多,增長的開銷就越小。
3. 對於UPDATE,要把這個估計值加倍。
在大多數狀況下,這將是一個很好的估計。UPDATE的估計值加倍只是一個猜想,實際上這取決於你修改了多少數據。之因此加倍,是由於在此假設要取一個X字節的行,並把它更新(UPDATE)爲另外一個X字節的行。若是你取一個小行(數據量較少的行),要把它更新爲一個大行(數據量較多的行),就不用對這個值加倍(這更像是一個INSERT)。若是取一個大行,而把它更新爲一個小行,也不用對這個值加倍(這更像是一個DELETE)。加倍只是一種「最壞狀況」,由於許多選項和特性會對此產生影響,例如,存在索引或者沒有索引也會影響這個底線。維護索引結構所必須的工做量對不一樣的UPDATE來講是不一樣的,此外還有一些影響因素。除了前面所述的固定開銷外,還必須把觸發器的反作用考慮在內。另外要考慮到Oracle爲你執行的隱式操做(如外鍵上的ON DELETE CASCADE設置)。有了這些考慮,你就能適當地估計redo量以便調整事務大小以及實現性能優化。
不能。由於重作日誌對於數據庫相當重要;它不是開銷,不是浪費。重作日誌對你來講確確實實必不可少。這是沒法改變的事實,也是數據庫採用的工做方式。若是你真的「關閉了redo」,那麼磁盤驅動器的任何暫時失敗、掉電或每一個軟件崩潰都會致使整個數據庫不可用,並且不可恢復。有些狀況下 執行某些操做時確實能夠不生成重作日誌。
有些SQL語句和操做支持使用NOLOGGING子句。這並非說:這個對象的全部操做在執行時都不生成重作日誌,而是說有些特定操做生成的redo會比日常(即不使用NOLOGGING子句時)少得多。只是說「redo」少得多,而不是「徹底沒有redo「。全部操做都會生成一些redo——不論日誌模式是什麼,全部數據字典操做都會計入日誌。只不過使用NOLOGGING子句後,生成的redo量可能會顯著減小。下面是使用NOLOGGING子句的一個例子。
爲此先在採用ARCHIVELOG模式運行的一個數據庫中運行如下命令:
sys@ORCL>select log_mode from v$database; LOG_MODE ------------ ARCHIVELOG sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size" NAME VALUE ---------------------------------------------------------------- ---------- redo size 0 redo size for lost write detection 0 redo size for direct writes 0 sys@ORCL>set echo off sys@ORCL>create table t 2 as 3 select * from all_objects; 表已建立。 sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat2 NAME V DIFF ---------------------------------------------------------------- ---------- ---- ------------ redo size 8535120 8,535,120 redo size for lost write detection 0 0 redo size for direct writes 8442060 8,442,060 sys@ORCL>set echo off
這個CREATE TABLE生成了大約8.5MB的redo信息。接下來刪除這個表,再重建,不過這一次採用NOLOGGING模式:
sys@ORCL>drop table t; 表已刪除。 sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size" NAME VALUE ---------------------------------------------------------------- ---------- redo size 8578096 redo size for lost write detection 0 redo size for direct writes 8442060 sys@ORCL>set echo off sys@ORCL>create table t 2 NOLOGGING 3 as 4 select * from all_objects; 表已建立。 sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat2 NAME V DIFF ---------------------------------------------------------------- ---------- ---------------- redo size 8663676 221,616 redo size for lost write detection 0 -8,442,060 redo size for direct writes 8443828 1,768 sys@ORCL>set echo off
這一次,只生成了221KB的redo信息。
能夠看到,差距很懸殊:原來有8.5MB的redo,如今只有221KB。8.5MB是實際的表數據自己;如今它直接寫至磁盤,對此沒有生成重作日誌。
若是對一個NOARCHIVELOG模式的數據庫運行這個測試,就看不到什麼差異。在NOARCHIVELOG模式的數據庫中,除了數據字典的修改外,CREATE TABLE不會記錄日誌。若是你想在NOARCHIVELOG模式的數據庫上看到差異,能夠把對錶T的DROP TABLE和CREATE TABLE換成DROP INDEX和CREATE INDEX。默認狀況下,不論數據庫以何種模式運行,這些操做都會生成日誌。從這個例子能夠得出一個頗有意義的提示:要按生產環境中所採用的模式來測試你的系統,由於不一樣的模式可能致使不一樣的行爲。你的生產系統可能採用AUCHIVELOG模式運行;假若你執行的大量操做在ARCHIVELOG模式下會生成redo,而在NOARCHIVELOG模式下不會生成redo,你確定想在測試時就發現這一點!
必須很是謹慎地使用NOLOGGING模式,並且要與負責備份和恢復的人溝通以後才能使用。下面假設你建立了一個非日誌模式的表,並做爲應用的一部分(例如,升級腳本中使用了CREATE TABLE AS SELECT NOLOGGING)。用戶白天修改了這個表。那天晚上,表所在的磁盤出了故障。「不要緊「,DBA說」數據庫在用ARCHIVELOG模式運行,咱們能夠執行介質恢復「。不過問題是,如今沒法從歸檔重作日誌恢復最初建立的表,由於根本沒有生成日誌,使得出現介質失敗後DBA沒法全面地恢復數據庫。這個表將沒法恢復。
關於NOLOGGING操做,須要注意如下幾點:
1. 事實上,仍是會生成必定數量的redo。這些redo的做用是保護數據字典。這是不可避免的。與之前(不使用NOLOGGING)相比,儘管生成的redo量要少多了,可是確實會有一些redo。
2. NOLOGGING不能避免全部後續操做生成redo。在前面的例子中,我建立的並不是不生成日誌的表。只是建立表(CREATE TABLE)這一個操做沒有生成日誌。全部後續的「正常「操做(如INSERT、UPDATE和DELETE)仍是會生成日誌。其餘特殊的操做(如使用SQL*Loader的直接路徑加載,或使用INSERT /*+ APPEND */語法的直接路徑插入)不生成日誌(除非你ALTER這個表,再次啓用徹底的日誌模式)。不過,通常來講,應用對這個表執行的操做都會生成日誌。
3. 在一個ARCHIVELOG模式的數據庫上執行NOLOGGING操做後,必須儘快爲受影響的數據文件創建一個新的基準備份,從而避免因爲介質失敗而丟失對這些對象的後續修改。實際上,咱們並不會丟失後來作出的修改,由於這些修改確實在重作日誌中;咱們真正丟失的只是要應用這些修改的數據(即最初的數據)。
使用NOLOGGING選項有兩種方法。1. NOLOGGING關鍵字潛在SQL命令中。2. 在段(索引或表)上設置NOLOGGING屬性,從而隱式地採用NOLOGGING模式來執行操做。例如,能夠把一個索引或表修改成默認採用NOLOGGING模式。這說明,之後重建這個索引不會生成日誌(其餘索引和表自己可能還會生成redo,可是這個索引不會):
sys@ORCL>create index t_idx on t(object_name); 索引已建立。 sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size" NAME VALUE ---------------------------------------------------------------- ---------- redo size 11644980 redo size for lost write detection 0 redo size for direct writes 11389496 sys@ORCL>set echo off
sys@ORCL>alter index t_idx rebuild; 索引已更改。 sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat2 NAME V DIFF ---------------------------------------------------------------- ---------- ---------------- redo size 14635016 3,245,520 redo size for lost write detection 0 -11,389,496 redo size for direct writes 14335208 2,945,712 sys@ORCL>set echo off
這個索引採用LOGGING模式(默認),重建這個索引會生成 3.2MB 的重作日誌。不過,能夠以下修改這個索引:
sys@ORCL>alter index t_idx nologging; 索引已更改。 sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size" NAME VALUE ---------------------------------------------------------------- ---------- redo size 14637616 redo size for lost write detection 0 redo size for direct writes 14335208 sys@ORCL>set echo off sys@ORCL>alter index t_idx rebuild; 索引已更改。 sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat2 NAME V DIFF ---------------------------------------------------------------- ---------- ---------------- redo size 14685008 349,800 redo size for lost write detection 0 -14,335,208 redo size for direct writes 14340044 4,836 sys@ORCL>set echo off
如今它只生成349KB的redo。可是,如今這個索引沒有獲得保護(unprotected),若是它所在的數據文件失敗而必須從一個備份恢復,咱們就會丟失這個索引數據。如今索引是不可恢復的,因此須要作一個備份。或者,DBA也能夠乾脆建立索引,由於徹底能夠從表數據直接建立索引。
能夠採用NOLOGGING模式執行如下操做:
1. 索引的建立和ALTER(重建)。
2. 表的批量INSERT(經過/*+APPEND */提示使用「直接路徑插入「。或採用SQL*Loader直接路徑加載)。表數據不生成redo,可是全部索引修改會生成redo,可是全部索引修改會生成redo(儘管表不生成日誌,但這個表上的索引卻會生成redo!)。
3. LOB操做(對大對象的更新沒必要生成日誌)。
4. 經過CREATE TABLE AS SELECT建立表。
5. 各類ALTER TABLE操做,如MOVE和SPLIT。
在一個ARCHIVELOG模式的數據庫上,若是NOLOGGING使用得當,能夠加快許多操做的速度,由於它能顯著減小生成的重作日誌量。假設你有一個表,須要從一個表空間移到另外一個表空間。能夠適當地調度這個操做,讓它在備份以後緊接着發生,這樣就能把表ALTER爲NOLOGGING模式,移到表,建立索引(也不生成日誌),而後再把表ALTER回LOGGING模式。如今,原先須要X小時才能完成的操做可能只須要X/2小時。要想適當地使用這個特性,須要DBA的參與,或者必須與負責數據庫備份和恢復(或任何備用數據庫)的人溝通。
Thread 1 cannot allocate new log, sequence 1466 Checkpoint not complete Current log# 3 seq# 1465 mem# 0: /home/ora10g/oradata/ora10g/redo03.log
警告消息中也可能指出Archival required而不是Checkpoint not complete,可是效果幾乎都同樣。若是數據庫試圖重用一個在線重作日誌文件,可是發現作不到,就會把這樣一條消息寫到服務器上的alert.log中。若是DBWR尚未完成重作日誌所保護數據的檢查點(checkpointing),或者ARCH尚未把重作日誌文件複製到歸檔目標,就會發生這種狀況。對最終用戶來講,這個時間點上數據庫實際上中止了。它會原地不動。DBWR或ARCH將獲得最大的優先級以將redo塊刷新輸出的磁盤。完成了檢查點或歸檔以後,一切又迴歸正常。數據庫之因此暫停用戶的活動,這是由於此時已經沒地方記錄用戶所作的修改了。Oracle試圖重用一個在線重作日誌文件,可是因爲歸檔進程還沒有完成這個文件的複製(Archival required),因此Oracle必須等待(相應地,最終用戶也必須等待),直到能安全地重用這個重作日誌文件爲止。
若是你看到會話由於一個「日誌文件切換」、「日誌緩衝區空間」或「日誌文件切換檢查點或歸檔未完成」等待了很長時間,就極可能遇到了這個問題。若是日誌文件大小不合適,或者DBWR和ARCH太慢(須要由DBA或系統管理員調優),在漫長的數據庫修改期間,你就會注意到這個問題。「起始」數據庫通常會把重作日誌的大小定得過小,不適用較大的工做量(包括數據字典自己的起始數據庫構建)。一旦啓動數據庫的加載,你會注意到,前1,000行進行得很快,而後就會呈噴射狀進行:1,000進行得很快,而後暫停,接下來又進行得很快,而後又暫停,如此等等。這些就是很明確的提示,說明你遭遇了這個問題。
要解決這個問題,有幾種作法:
1. 讓DBWR更快一些。讓你的DBA對DBWR調優,爲此能夠啓用ASYNC I/O、使用DBWR I/O從屬進程,或者使用多個DBWR進程。看看系統產生的I/O,查看是否有一個磁盤(或一組磁盤)「太熱」,相應地須要將數據散佈開。這個建議對ARCH也適用。這種作法的好處是,你不用付出什麼代價就能有所收穫,性能會提升,並且沒必要修改任何邏輯/結構/代碼。
2. 增長更多重作日誌文件。在某些狀況下,這會延遲Checkpoint not complete的出現,並且過一段時間後,能夠把Checkpoint not complete延遲得足夠長,使得這個錯誤可能根本不會出現(由於你給DBWR留出了足夠的活動空間來創建檢查點)。這個方法也一樣適用於Archival required消息。這種方法的好處是能夠消除系統中的「暫停」。其缺點是會消耗更多的磁盤空間。
3. 從新建立更大的日誌文件。這會擴大填寫在線重作日誌與重用這個在線重作日誌文件之間的時間間隔。若是重作日誌文件的使用呈「噴射狀」,這種方法一樣適用於Archival required消息。假若一段時間內會大量生成日誌(如每晚加載、批處理等),其後一段數據卻至關平靜,若是有更大的在線重作日誌,就能讓ARCH在平靜的期間有足夠的時間「遇上來」。這種方法的優缺點與前面增長更多文件的方法是同樣的。另外,它可能會延遲檢查點的發生,因爲(至少)每一個日誌切換都會發生檢查點,而如今日誌切換間隔會更大。
4. 讓檢查點發生得更頻繁、更連續。能夠使用一個更小的塊緩衝區緩存(不太好),或者使用諸如FAST_START_MTTR_TARGET、LOG_CHECKPOINT_INTERVAL和LOG_CHECKPOINT_TIMEOUT之類的參數設置。這會強制DBWR更 頻繁地刷新輸出髒塊。這種方法的好處是,失敗恢復的時間會減小。在線重作日誌中應用的工做確定更少。其缺點是,若是常常修改塊,可能會更頻繁地寫至磁盤。 緩衝區緩存本該更有效的,但因爲頻繁地寫磁盤,會致使緩衝區緩存不能充分發揮做用,這可能會影響塊清除機制。
塊清除(block cleanout),即生成所修改數據庫塊上與「鎖定」有關的信息。
數據鎖其實是數據的屬性,存儲在塊首部。下一次訪問這個塊時,可能必須「清理」這個塊,要將這些事務信息刪除。這個動做會生成redo,並致使變髒(本來並不髒,由於數據自己沒有修改),這說明一個簡單的SELECT有可能生成redo,並且可能致使完成下一個檢查點時將大量的塊寫至磁盤。不過,在大多數正常的狀況下,這是不會發生的。若是系統中主要是小型或中型事務(OLTP),或者數據倉庫會執行直接路徑加載或使用DBMS_STATS在加載操做後分析表,你會發現塊一般已經獲得「清理」。COMMIT時處理的步驟之一是:若是塊還在SGA中,就要再次訪問這些塊,若是能夠訪問(沒有別人在修改這些塊),則對這些塊完成清理。這個活動稱爲提交清除(commit cleanout),即清除已修改塊上事務信息。最理想的是,COMMIT能夠完成塊清除,這樣後面的SELECT(讀)就沒必要再清理了。只有塊的UPDATE纔會真正清除殘餘的事務信息,因爲UPDATE已經在生成redo,所用注意不到這個清除工做。
能夠強制清除不發生來觀察它的反作用,並瞭解提交清除是怎麼工做的。在與咱們的事務相關的提交列表中,Oracle會記錄已修改的塊列表。這些列表都有20個塊,Oracle會根據須要分配多個這樣的列表,直至達到某個臨界點。若是咱們修改的塊加起來超過了塊緩衝區緩存大小的10%,Oracle會中止爲咱們分配新的列表。例如,若是緩衝區緩存設置爲能夠緩存3,000個塊,Oracle會爲咱們維護最多300個塊(3,000的10%)。COMMIT時,Oracle會處理這些包含20個塊指針的列表,若是塊仍可用,它會執行一個很快的清理。因此,只要咱們修改的塊數沒有超過緩存中總塊數的10%,並且塊仍在緩存中而且是可用的,Oracle就會在COMMIT時清理這些塊。不然,它只會將其忽略(也就是說不清理)。
在下表中填入了5000行,並COMMIT。測量到此爲止生成的redo量。而後運行一個SELECT,它會訪問每一個塊,最後測量這個SELECT生成的redo量。
SELECT會生成redo。不只如此,它還會把這些修改塊「弄髒」,致使DBWR再次將塊寫入磁盤。這是由於塊清除的緣故。接下來,我會再一次運行SELECT,能夠看到這回沒有生成redo。這在乎料之中,由於此時塊都已經「乾淨」了。
sys@ORCL>create table t 2 ( x char(2000), 3 y char(2000), 4 z char(2000) 5 ) 6 / 表已建立。 sys@ORCL>insert into t 2 select 'x', 'y', 'z' 3 from all_objects 4 where rownum <= 50000; 已建立50000行。 統計信息 ---------------------------------------------------------- 4152 recursive calls 233091 db block gets 64054 consistent gets 564 physical reads 326484496 redo size 1137 bytes sent via SQL*Net to client 1317 bytes received via SQL*Net from client 4 SQL*Net roundtrips to/from client 31 sorts (memory) 0 sorts (disk) 50000 rows processed sys@ORCL>commit; 提交完成。
上述表 每一個塊中包含一行(個人數據庫中塊大小爲8KB)。
scott@ORCL> show parameter db_block_size NAME TYPE VALUE ------------------------------------ ----------- ------------------------------ db_block_size integer 8192
如今測量讀數據是生成的redo量:
sys@ORCL>select * 2 from t; 已選擇50000行。 統計信息 ---------------------------------------------------------- 5 recursive calls 0 db block gets 100076 consistent gets 50116 physical reads 4004 redo size 302570630 bytes sent via SQL*Net to client 37182 bytes received via SQL*Net from client 3335 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 50000 rows processed
可見,這個SELECT在處理期間生成了大約 4KB 的redo。這表示對T進行全表掃描時修改了 4KB的塊首部。DBWR會在未來某個時間把這些已修改的塊寫回到磁盤上。如今,若是再次運行這個查詢:
sys@ORCL>select * 2 from t; 已選擇50000行。 統計信息 ---------------------------------------------------------- 0 recursive calls 0 db block gets 99951 consistent gets 50001 physical reads 0 redo size 302570630 bytes sent via SQL*Net to client 37182 bytes received via SQL*Net from client 3335 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 50000 rows processed
能夠看到,這一次沒有生成redo,塊都是乾淨的。
若是把緩衝區緩存設置爲能保存至少50,000個塊,再次運行前面的例子。會發現,不管哪個SELECT,生成的redo都不多甚至沒有——咱們沒必要在其中任何一個SELECT語句期間清理髒塊。這是由於,咱們修改的5000個塊徹底能夠在緩衝區緩存的10%中放下,並且咱們是獨家用戶。別人不會動數據,不會有人致使咱們的數據刷新輸出到磁盤,也沒有人在訪問這些塊。在實際系統中,有些狀況下,至少某些塊不會進行清除,這是正常的。
若是執行一個大的INSERT(如上所述)、UPDATE或DELETE,這種塊清除行爲的影響最大,它會影響數據庫中的許多塊(緩存中10%以上的塊都會完成塊清除)。在此以後,第一個「接觸」塊的查詢會生成少許的redo,並把塊弄髒,若是DBWR已經將塊刷新輸出或者實例已經關閉,可能就會由於這個查詢而致使重寫這些塊,並徹底清理緩衝區緩存。若是Oracle不對塊完成這種延遲清除,那麼COMMIT的處理就會與事務自己同樣長。COMMIT必須從新訪問每個塊,可能還要從磁盤將塊再次讀入(它們可能已經刷新輸出)。
假設你更新(UPDATE)了大量數據,而後COMMIT。如今對這些數據運行一個查詢來驗證結果。看上去查詢生成了大量寫I/O和redo。
在一個OLTP系統中,可能歷來不會看到這種狀況發生,由於OLTP系統的特色是事務都很短小,只會影響爲數很少的一些塊。根據設計,全部或者大多數事務都短而精。只是修改幾個塊,並且這些塊都會獲得清理。在一個數據倉庫中,若是加載以後要對數據執行大量UPDATE,就要把塊清除做爲設計中要考慮的一個因素。有些操做會在「乾淨」的塊上建立數據。例如,CREATE TABLE AS SELECT、直接路徑加載的數據以及直接路徑插入的數據都會建立「乾淨」的塊。UPDATE、正常的INSERT或DELETE建立的塊則可能須要在第一次讀時完成塊清除。若是你有以下的處理,就會受到塊清除的影響:
1. 將大量新數據批量加載到數據倉庫中;
2. 在剛剛加載的全部數據上運行UPDATE(產生須要清理的塊);
3. 讓人們查詢這些數據。
若是塊須要清理,第一接觸這個數據的查詢將帶來一些額外的處理。
應該在UPDATE之 後本身主動地「接觸」數據。你剛剛加載或修改了大量的數據;如今至少須要分析這些數據。可能要自行運行一些報告來驗證數據已經加載。這些報告會完成塊清 除,這樣下一個查詢就沒必要再作這個工做了。更好的作法是:因爲你剛剛批量加載了數據,如今須要以某種方式刷新統計。經過運行DBMS_STATS實用程序來收集統計,就能很好地清理全部塊,這是由於它只是使用SQL來查詢信息,會在查詢當中很天然地完成塊清除。
若是你遭遇到日誌競爭,可能會看到對「日誌文件同步」事件的等待時間至關長,另外Statspack報告的「日誌文件並行寫」事件中寫次數(寫I/O數)可能很大。若是觀察到這種狀況,就說明你遇到了重作日誌的競爭;重作日誌寫得不夠快。發生這種狀況可能有許多緣由。其中一個應用緣由是:提交得太過頻繁,例如在重複執行INSERT的循環中反覆提交。若是提交得太頻繁,這不只是很差的編程實踐,確定還會引入大量日誌文件同步等待。假設你的全部事務都有適當的大小(徹底聽從業務規則的要求,而沒有過於頻繁地提交),但仍是看到了這種日誌文件等待,這就有其餘緣由了。其中最多見的緣由以下:
1. redo放在一個慢速設備上:磁盤表現不佳。
2. redo與其餘頻繁訪問的文件放在同一個設備上。redo設計爲要採用順序寫,並且要放在專用的設備上。若是系統的其餘組件(甚至其餘Oracle組件)試圖與LGWR同時讀寫這個設備,你就會遭遇某種程度的競爭。在此,只要有可能,你就會但願確保LGWR擁有這些設備的獨佔訪問權限。
3. 已緩衝方式裝載日誌設備。你在使用一個「cooked」文件系統(而不是RAW磁盤)。操做系統在緩衝數據,而數據庫也在緩衝數據(重作日誌緩衝區)。這種雙緩衝會讓速度慢下來。若是可能,應該以一種「直接」方式了裝載設備。具體操做依據操做系統和設備的不一樣而有所變化,但通常均可以直接裝載。
4. redo採用了一種慢速技術,如RAID-5。RAID-5很合適讀,可是用於寫時表現則不好。COMMIT期間 咱們必須等待LGWR以確保數據寫到磁盤上。
只有有可能,實際上你會但願至少有5個專用設備來記錄日誌,最好還有第6個設備來鏡像歸檔日誌。因爲當前每每使用9GB、20GB、36GB、200GB、300GB和更大的磁盤,要想擁有這麼多專用設備變得更加困難。可是若是能留出4塊你能找到的最小、最快的磁盤,再有一個或兩個大磁盤,就能夠很好地促進LGWR和ARCH的工做。
在線重作日誌文件是一組Oracle文件,最適合使用RAW磁盤(原始磁盤)。在線重作日誌文件不用備份,因此將在線重作日誌文件放在RAW分區上而不是cooked文件系統上,這不會影響你的任何備份腳本。ARCH總能把RAW日誌轉變爲cooked文件系統文件(不能使用一個RAW設備來創建歸檔)。
臨時表不會爲它們的塊生成redo。所以,對臨時表的操做不是「可恢復的」。修改臨時表中的一個塊時,不會將這個修改記錄到重作日誌文件中。不過,臨時表確實會生成undo,並且這個undo會計入日誌。所以,臨時表也會生成一些redo。這是由於你能回滾到事務中的一個SAVEPOINT。能夠擦除對臨時表的後50個INSERT,而只留下前50個。臨時表能夠有約束,正常表有的一切臨時表均可以有。可能有一條INSERT語句要向臨時表中插入500行,但插入到第500行時失敗了,這就要求回滾這條語句。因爲臨時表通常表現得就像「正常」表同樣,因此臨時表必須生成undo。因爲undo數據必須創建日誌,所以臨時表會爲所生成的undo生成一些重作日誌。
在臨時表上運行的SQL語句主要是INSERT和SELECT。幸運的是,INSERT只生成極少的undo(須要把塊恢復爲插入前的「沒有」狀態,而存儲「沒有」不須要多少空間),另外SELECT根本不生成undo。
我創建了一個小測試來演示使用臨時表時生成的redo量,同時這也暗示了臨時表生成的undo量,由於對於臨時表,只會爲undo生成日誌。爲了說明這一點,我採用了配置相同的「永久」表和「臨時」表,而後對各個表執行相同的操做,測量每次生成的redo量。這裏使用的表以下:
scott@ORCL>create table perm 2 ( x char(2000) , 3 y char(2000) , 4 z char(2000) ) 5 / 表已建立。 scott@ORCL>create global temporary table temp 2 ( x char(2000) , 3 y char(2000) , 4 z char(2000) ) 5 on commit preserve rows 6 / 表已建立。
創建了一個小的存儲過程,它能執行任意的SQL,並報告SQL生成的redo量。
使用這個例程分別在臨時表和永久表上執行INSERT、UPDATE和DELETE:
scott@ORCL>create or replace procedure do_sql( p_sql in varchar2 ) 2 as 3 l_start_redo number; 4 l_redo number; 5 begin 6 select v$mystat.value 7 into l_start_redo 8 from v$mystat, v$statname 9 where v$mystat.statistic# = v$statname.statistic# 10 and v$statname.name = 'redo size'; 11 12 execute immediate p_sql; 13 commit; 14 15 select v$mystat.value-l_start_redo 16 into l_redo 17 from v$mystat, v$statname 18 where v$mystat.statistic# = v$statname.statistic# 19 and v$statname.name = 'redo size'; 20 21 dbms_output.put_line 22 ( to_char(l_redo,'9,999,999') ||' bytes of redo generated for "' || 23 substr( replace( p_sql, chr(10), ' '), 1, 25 ) || '"...' ); 24 end; 25 / 過程已建立。
接下來,對PERM表和TEMP表運行一樣的INSERT、UPDATE和DELETE:
scott@ORCL>set serveroutput on format wrapped scott@ORCL>begin 2 do_sql( 'insert into perm 3 select 1,1,1 4 from all_objects 5 where rownum <= 500' ); 6 7 do_sql( 'insert into temp 8 select 1,1,1 9 from all_objects 10 where rownum <= 500' ); 11 dbms_output.new_line; 12 13 do_sql( 'update perm set x = 2' ); 14 do_sql( 'update temp set x = 2' ); 15 dbms_output.new_line; 16 17 do_sql( 'delete from perm' ); 18 do_sql( 'delete from temp' ); 19 end; 20 / 3,293,240 bytes of redo generated for "insert into perm select"... 66,432 bytes of redo generated for "insert into temp select"... 2,182,288 bytes of redo generated for "update perm set x = 2"... 1,100,296 bytes of redo generated for "update temp set x = 2"... 3,215,200 bytes of redo generated for "delete from perm"... 3,215,328 bytes of redo generated for "delete from temp"... PL/SQL 過程已成功完成。
能夠看到:
1. 對「實際」表(永久表)的INSERT生成了大量redo。而對臨時表幾乎沒有生成任何redo。對臨時表的INSERT只會生成不多的undo數據,並且對於臨時表只會爲undo數據創建日誌。
2. 實際表的UPDATE生成的redo大約是臨時表更新所生成redo的兩倍。必須保存UPDATE的大約一半(即「前映像」)。對於臨時表來講,沒必要保存「後映像」(redo)。
3. DELETE須要幾乎相同的redo空間。由於對DELETE的undo很大,而對已修改塊的redo很小。所以,對臨時表的DELETE與對永久表的DELETE幾乎相同。
關於臨時表上的DML活動,能夠得出如下通常結論:
1. INSERT會生成不多甚至不生成undo/redo活動
2. DELETE在臨時表上生成的redo與正常表上生成的redo一樣多
3. 臨時表的UPDATE會生成正常表UPDATE一半的redo
對於最後一個結論,須要指出有一些例外狀況。例如,若是我用2,000字節的數據UPDATE(更新)徹底爲NULL的一列,生成的undo數據就很是少。這個UPDATE表現得就像是INSERT。另外一方面,若是我把有2,000字節數據的一列UPDATE爲全NULL,對redo生成來講,這就表現得像是DELETE。平均來說,能夠這樣認爲:臨時表UPDATE與實際表UPDATE生成的undo/redo相比,前者是後者的50%。
通常來說,關於建立的redo量有一個常識。若是你完成的操做致使建立undo數據,則能夠肯定逆向完成這個操做(撤銷操做)的難易程度。若是INSERT2,000字節,逆向操做就很容易,只需回退到無字節便可。若是刪除了(DELETE)2,000字節,逆向操做就是要插入2,000字節。在這種狀況下,redo量就很大。
避免刪除臨時表。能夠使用TRUNCATE,或者只是讓臨時表在COMMIT以後或會話終止時自動置空。執行方法不會生成undo,相應地也不會生成redo。你可能會盡可能避免更新臨時表,除非因爲某種緣由必須這樣作。把臨時表主要用於插入(INSERT)和選擇(SELECT)。採用這種方式,就能更優地使用臨時表不生成redo的特有能力。
若是存在索引(或者實際上表就是索引組織表),這將顯著地影響生成的undo量,由於索引是一種複雜的數據結構,可能會生成至關多的undo信息。
通常來說,INSERT生成的undo最少,由於Oracle爲此需記錄的只是要「刪除」的一個rowid(行ID)。UPDATE通常排名第二(在大多數狀況下)。對於UPDATE,只需記錄修改的字節。通常只更新(UPDATE)了整個數據行中不多的一部分。所以,必須在undo中記錄行的一小部分。通常來說,DELETE生成的undo最多。對於DELETE,Oracle必須把整行的前映像記錄到undo段中。INSERT只生成須要創建日誌的不多的undo。UPDATE生成的undo量等於所修改數據的前映像大小,DELETE會生成整個數據集寫至undo段。
與加索引列的更新相比,對一個未加索引的列進行更新不只執行得更快,生成的undo也會好得多。例如,下面建立一個有兩列的表,這兩列包含相同的數據,可是其中一列加了索引:
scott@ORCL>create table t 2 as 3 select object_name unindexed, 4 object_name indexed 5 from all_objects 6 / 表已建立。 scott@ORCL>create index t_idx on t(indexed); 索引已建立。 scott@ORCL>exec dbms_stats.gather_table_stats(user,'T'); PL/SQL 過程已成功完成。
下面更新這個表,首先,更新未加索引的列,而後更新加索引的列。咱們須要一個新的V$查詢來測量各類狀況下生成的undo量。如下查詢能夠完成這個工做。它先從V$MYSTAT獲得咱們的會話ID(SID),在使用這個會話ID在V$SESSION視圖中找到相應的會話記錄,並獲取事務地址(TADDR)。而後使用TADDR拉出(查出)咱們的V$TRANSACTION記錄(若是有),選擇USED_UBLK列,即已用undo塊的個數。因爲咱們目前不在一個事務中,這個查詢如今應該返回0行:
scott@ORCL>select used_ublk 2 from v$transaction 3 where addr = (select taddr 4 from v$session 5 where sid = (select sid 6 from v$mystat 7 where rownum = 1 ) 8 ) 9 / 未選定行
而後在每一個UPDATE以後再使用這個查詢:
scott@ORCL>update t set unindexed = lower(unindexed); 已更新71869行。 scott@ORCL>select used_ublk 2 from v$transaction 3 where addr = (select taddr 4 from v$session 5 where sid = (select sid 6 from v$mystat 7 where rownum = 1 ) 8 ) 9 / USED_UBLK ---------- 1229 scott@ORCL>commit; 提交完成。 scott@ORCL>select used_ublk 2 from v$transaction 3 where addr = (select taddr 4 from v$session 5 where sid = (select sid 6 from v$mystat 7 where rownum = 1 ) 8 ) 9 / 未選定行
這個UPDATE使用了1229個塊存儲其undo。提交會「解放」這些塊,或者將其釋放,因此若是再次對V$TRANSACTION運行這個查詢,它還會顯示no rows selected。
更新一樣的數據時,不過這一次是加索引的列,會觀察到下面的結果:
scott@ORCL>update t set indexed = lower(indexed); 已更新71869行。 scott@ORCL>select used_ublk 2 from v$transaction 3 where addr = (select taddr 4 from v$session 5 where sid = (select sid 6 from v$mystat 7 where rownum = 1 ) 8 ) 9 / USED_UBLK ---------- 2763 scott@ORCL>commit; 提交完成。 scott@ORCL>select used_ublk 2 from v$transaction 3 where addr = (select taddr 4 from v$session 5 where sid = (select sid 6 from v$mystat 7 where rownum = 1 ) 8 ) 9 / 未選定行
更新加索引的列會生成 2倍多的undo。這是由於索引結構自己所固有的複雜性,並且咱們更新了這個表中的每一行,移動了這個結構中的每個索引鍵值。
致使這個錯的一個緣由:提交得太過頻繁。
1. undo段過小,不足以在系統上執行工做。
2. 程序跨COMMIT獲取(實際上這是前一點的一個變體)。
3. 塊清除
前兩點與Oracle的讀一致性模型直接相關。查詢的結果是預約的,在Oracle去獲取第一行以前,結果就已經定好了。Oracle使用undo段來回滾自查詢開始以來有修改的塊,從而提供數據庫的一致時間點「快照」。例如執行如下語句:
update t set x = 5 where x = 2; insert into t select * from t where x = 2; delete from t where x = 2; select * from t where x = 2;
執行每條語句時都會看到T的一個讀一致視圖以及X=2的行集,而不論數據庫中還有哪些併發的活動。
全部「讀」這個表的語句都利用了這種讀一致性。在上面所示的例子中,UPDATE讀這個表,找到X=2的行(而後UPDATE這些行)。INSERT也要讀表,找到X=2的行,而後INSERT,等等。因爲兩個語句都使用了undo段,都是爲了回滾失敗的事務並提供讀一致性,這就致使了ORA-01555錯誤。
前面列的第三項也會致使ORA-01555,由於它可能在只有一個會話的數據庫中發生,並且這個會話並無修改出現ORA-01555錯誤時所查詢的表!
ORA-01555錯誤的幾種解決方案,通常來講能夠採用下面的方法:
1. 適當地設置參數UNDO_RETENTION(要大於執行運行時間最長的事務所需的時間)。能夠用V$UNDOSTAT來肯定長時間運行的查詢的持續時間。另外,要確保磁盤上已經預留了足夠的空間,使undo段能根據所請求的UNDO_RETENTION增大。
2. 使用手動undo管理時加大或增長更多的回滾段。這樣在長時間運行的查詢執行期間,覆蓋undo數據的可能性就能下降。
3. 減小查詢的運行時間(調優)。這樣就能下降對undo段的需求,不需求太大的undo段。
4. 收集相關對象的統計信息。因爲大批量的UPDATE或INSERT會致使塊清除(block cleanout),因此須要在大批量UPDATE或大量加載以後以某種方式收集統計信息。
一種場景是:你的系統中事務很小。正因如此,只須要分配很是少的undo段空間。假如,假設存在如下狀況:
每一個事務平均生成8KB的undo。
平均每秒完成其中5個事務(每秒生成40KB的undo,每分鐘生成2,400KB的undo)。
有一個生成1MB undo的事務平均每分鐘出現一次。總的說來,每分鐘會生成大約3.5MB的undo。
你爲系統配置了15MB的undo。
處理事務時,相對於這個數據庫的undo需求,這徹底夠了。undo段會迴繞,平均每3~4分鐘左右會重用一次undo段空間。
不過,在一樣的環境中,可能有一些報告需求。其中一些查詢須要運行至關長的時間,多是5分鐘。這就有問題了。若是這些查詢須要執行5分鐘,並且它們須要查詢開始時的一個數據視圖,你就極有可能遭遇ORA-01555錯誤。因爲你的undo段會在這個查詢執行期間迴繞,要知道查詢開始以來生成的一些undo信息已經沒有了,這些信息已經被覆蓋。若是你命中了一個塊,而這個塊幾乎在查詢開始的同時被修改,這個塊的undo信息就會由於undo段迴繞而丟掉,你將收到一個ORA-01555錯誤。
如下是一個小例子。假設咱們有一個表,其中有塊一、二、三、…、1,000,000。下表顯示了可能出現的事件序列。
長時間運行的查詢時間表
時間(分:秒) 動做
0:00 查詢開始
0:01 另外一個會話更新(UPDATE)塊1,000,000。將塊1,000,000的undo信息記錄到某個undo段
0:01 這個UPDATE會話提交(COMMIT)。它生成的undo數據還在undo段中,可是假若咱們須要空間, 選擇容許覆蓋這個信息
1:00 咱們的查詢還在運行。如今更新到塊200,000
1:01 進行了大量活動。如今已經生成了稍大於14MB的undo
3:00 查詢還在兢兢業業地工做着。如今處理到塊600,000左右
4:00 undo段開始迴繞,並重用查詢開始時(0:00)活動的空間。具體地講,咱們已經重用了原先0:01時刻 UPDATE 塊1,000,000時所用的undo段空間
5:00 查詢終於到了塊1,000,000。它發現自查詢開始以來這個塊已經修改過。它找到undo段,試圖發現對應這一塊的undo來獲得一個一致讀。此時,它發現所須要的信息已經不存在了。這就產生了ORA-01555錯誤,查詢失敗
具體就是這樣的。若是如此設置undo段大小,使得頗有可能在執行查詢期間重用這些undo段,並且查詢要訪問被修改的數據,那就也有可能不斷地遭遇ORA-01555錯誤。此時必須把UNDO_RETENTION參數設置得高一些,讓Oracle負責肯定要保留多少undo段的大小,讓它們更大一些(或者有更多的undo段)。你要配置足夠的undo,在長時間運行的查詢期間應當可以維持。在前面的例子中,只是針對修改數據的事務來肯定系統undo段的大小,而忘記了還有考慮系統的其餘組件。
假若手動地管理undo段,undo段歷來不會由於查詢而擴大;只有INSERT、UPDATE和DELETE纔會讓undo段增加。只有當執行一個長時間運行的UPDATE事務時纔會擴大手動回滾段。
爲了演示這種效果,咱們將建立一個很是小的undo表空間,並有一個生成許多小事務的會話,實際上這能確保這個undo表空間迴繞,屢次重用所分配的空間,而不論UNDO_RETENTION設置爲多大,由於咱們不容許undo表空間增加。使用這個undo段的會話將修改一個表T。它使用T的一個全表掃描,自頂向下地讀表;在另外一個會話中,執行一個查詢,它經過一個索引讀表T,這個查詢會稍微有些隨機地讀表:先讀第1行,而後是第1,000行,接下來是第500行,再後面是第20,001行,如此等等。這樣一來,咱們可能會很是隨機地訪問塊,並在查詢的處理期間屢次訪問塊。這種狀況下獲得ORA-01555錯誤的機率幾乎是100%。因此,在一個會話中首先執行如下命令:
scott@ORCL>create undo tablespace undo_small 2 datafile 'D:\app\Administrator\oradata\orcl\undosmall.dbf' size 2m 3 autoextend off 4 / 表空間已建立。 scott@ORCL>alter system set undo_tablespace = undo_small; 系統已更改。
如今,創建表T來查詢和修改。注意 在這個表中隨機地對數據排序。CREATE TABLE AS SELECT力圖按查詢獲取的順序將行放在塊中。咱們的目的只是把行弄亂,使它們不至於認爲地有某種順序,從而獲得隨機的分佈:
scott@ORCL>create table t 2 as 3 select * 4 from all_objects 5 order by dbms_random.random; 表已建立。 scott@ORCL>alter table t add constraint t_pk primary key(object_id) 2 / 表已更改。 scott@ORCL>exec dbms_stats.gather_table_stats( user, 'T', cascade=> true ); PL/SQL 過程已成功完成。
如今能夠執行修改了:
scott@ORCL>begin 2 for x in ( select rowid rid from t ) 3 loop 4 update t set object_name = lower(object_name) where rowid = x.rid; 5 commit; 6 end loop; 7 end; 8 /
在運行這個修改的同時,在另外一個會話中運行一個查詢。這個查詢要讀表T,並處理每一個記錄。獲取下一個記錄以前處理每一個記錄所花的時間大約爲1/100秒(使用DBMS_LOCK.SLEEP(0.01)來模擬)。在查詢中使用了FIRST_ROWS提示,使之使用前面建立的索引,從而經過索引(按OBJECT_ID排序)來讀出表中的行。因爲數據是隨機地插入到表中的,咱們可能會至關隨機地查詢表中的塊。這個查詢只運行幾秒就會失敗:
scott@ORCL>declare 2 cursor c is 3 select /*+ first_rows */ object_name 4 from t 5 order by object_id; 6 7 l_object_name t.object_name%type; 8 l_rowcnt number := 0; 9 begin 10 open c; 11 loop 12 fetch c into l_object_name; 13 exit when c%notfound; 14 dbms_lock.sleep( 0.01 ); 15 l_rowcnt := l_rowcnt+1; 16 end loop; 17 close c; 18 exception 19 when others then 20 dbms_output.put_line( 'rows fetched = ' || l_rowcnt ); 21 raise; 22 end; 23 / rows fetched = 56 declare * 第 1 行出現錯誤: ORA-01555: 快照過舊: 回退段號 32 (名稱爲 "_SYSSMU32_4133326864$") 太小 ORA-06512: 在 line 21
能夠看到,在遭遇ORA-01555:snapshot too old錯誤而失敗以前,它只處理了56個記錄。要修正這個錯誤,咱們要保證作到兩點:
1. 數據庫中UNDO_RETENTION要設置得足夠長,以保證這個讀進程完成。這樣數據庫就能擴大undo表空間來保留足夠的undo,使咱們可以完成工做。
2. undo表空間能夠增加,或者爲之手動分配更多的磁盤空間。
對於這個例子,我認爲這個長時間運行的進程須要大約600秒才能完成。個人UNDO_RETENTION設置爲900(單位是秒,因此undo保持大約15分鐘)。我修改了undo表空間的數據文件,使之一次擴大1MB,直到最大達到2GB:
scott@ORCL>column file_name new_val F scott@ORCL>select file_name 2 from dba_data_files 3 where tablespace_name = 'UNDO_SMALL'; FILE_NAME -------------------------------------------------------------------------------- D:\APP\ADMINISTRATOR\ORADATA\ORCL\UNDOSMALL.DBF scott@ORCL>alter database 2 datafile '&F' 3 autoextend on 4 next 1m 5 maxsize 2048m; 原值 2: datafile '&F' 新值 2: datafile 'D:\APP\ADMINISTRATOR\ORADATA\ORCL\UNDOSMALL.DBF' 數據庫已更改。
再次併發地運行這些進程時,兩個進程都能順利完成。這一次undo表空間的數據文件擴大了,由於在此容許undo表空間擴大,並且根據我設置的undo保持時間可知:
scott@ORCL>select bytes/1024/1024 2 from dba_data_files 3 where tablespace_name = 'UNDO_SMALL'; BYTES/1024/1024 --------------- 19
所以,這裏沒有收到錯誤,咱們成功地完成了工做,並且undo擴大得足夠大,能夠知足咱們的須要。在這個例子中,之因此會獲得錯誤只是由於咱們經過索引來讀表T,並且在全表上執行隨機讀。若是不是這樣,而是執行全表掃描,在這個特例中極可能不會遇到ORA-01555錯誤。緣由是SELECT和UPDATE都要對T執行全表掃描,而SELECT掃描極可能在UPDATE以前進行(SELECT只須要讀,而UPDATE不只要讀還有更新,所以可能更慢一些)。若是執行隨機讀,SELECT就更有可能要讀已修改的塊(即塊中的多行已經被UPDATE修改並且已經提交)。這就展現了ORA-01555的「陰險」,這個錯誤的出現取決於併發會話如何訪問和管理底層表。
塊清除是致使ORA-01555錯誤錯誤的緣由,儘管很難徹底杜絕,不過好在畢竟並很少見,由於可能出現塊清除的狀況不常發生。在塊清除過程當中,若是一個塊已被修改,下一個會話訪問這個塊時,可能必須查看最 後一個修改這個塊的事務是否仍是活動的。一旦肯定該事務再也不活動,就會完成塊清除,這樣另外一個會話訪問這個塊時就沒必要再歷經一樣的過程。要完成塊清除,Oracle會從塊首部肯定前一個事務所用的undo段,而後肯定從undo首部能不能看出這個塊是否已經提交。
能夠用如下兩種方式完成這種確認。
一種方式是Oracle能夠肯定這個事務好久之前就已經提交,它在undo段事務表中的事務槽已經被覆蓋。
另外一種狀況是COMMIT SCN還在undo段的事務表中,這說明事務只是稍早前剛提交,其事務槽還沒有被覆蓋。
要從一個延遲的塊清除收到ORA-01555錯誤,如下條件都必須知足:
1. 首先作了一個修改並COMMIT,塊沒有自動清理(即沒有自動完成「提交清除」,例如修改了太多的塊,在SGA塊緩衝區緩存的10%中放不下)。
2. 其餘會話沒有接觸這些塊,並且在咱們這個「倒黴」的查詢(稍後顯示)命中這些塊以前,任何會話都不會接觸它們。
3. 開始一個長時間運行的查詢。這個查詢最後會讀其中的一些塊。這個查詢從SCN t1開始,這就是讀一致SCN,必須將數據回滾到這一點來獲得讀一致性。開始查詢時,上述修改事務的事務條目還在undo段的事務表中。
4. 查詢期間,系統中執行了多個提交。執行事務沒有接觸執行已修改的塊。
5. 因爲出現了大量的COMMIT,undo段中的事務表要回繞並重用事務槽。最重要的是,將循環地重用原來修改事務的事務條目。另外,系統重用了undo段的區段,以免對undo段首部塊自己的一致讀。
6. 此外,因爲提交太多,undo段中記錄的最低SCN如今超過了t1(高於查詢的讀一致SCN)。
若是查詢到達某個塊,而這個塊在查詢開始以前已經修改並提交,就會遇到麻煩。正常狀況下,會回到塊所指的undo段,找到修改了這個塊的事務的狀態(換句話說,它會找到事務的COMMIT SCN)。若是這個COMMIT SCN小於t1,查詢就能夠使用這個塊。若是該事務的COMMIT SCN大於t1,查詢就必須回滾這個塊。不過,問題是,在這種特殊的狀況下,查詢沒法肯定塊的COMMIT SCN是大於仍是小於t1。相應地,不清楚查詢可否使用這個塊映像。這就致使了ORA-01555錯誤。
爲 了真正看到這種狀況,咱們將在一個表中建立多個須要清理的塊。而後在這個表上打開一個遊標,並容許對另外某個表完成許多小事務(不是那個剛更新並打開了遊標的表)。最後嘗試爲該遊標獲取數據。如今,咱們認爲遊標 應該能看到全部數據,由於咱們是在打開遊標以前完成並提交了表修改。假若此時 獲得ORA-01555錯誤,就說明存在前面所述的問題。要創建這個例子,咱們將使用:
1. 2MB UNDO_SMALL undo 表空間。
2. 4MB的緩衝區緩存,足以放下大約500個塊。這樣咱們就能夠將一些髒塊刷新輸出到磁盤來觀察這種現象。
scott@ORCL>create table big 2 as 3 select a.*, rpad('*',1000,'*') data 4 from all_objects a; 表已建立。 scott@ORCL>exec dbms_stats.gather_table_stats( user, 'BIG' ); PL/SQL 過程已成功完成。
因爲使用了這麼大的數據字段,每一個塊中大約有6~7行,因此這個表中有大量的塊。接下來,咱們建立將由多個小事務修改的小表:
scott@ORCL>create table small ( x int, y char(500) ); 表已建立。 scott@ORCL>insert into small select rownum, 'x' from all_users; 已建立39行。 scott@ORCL>commit; 提交完成。 scott@ORCL>exec dbms_stats.gather_table_stats( user, 'SMALL' ); PL/SQL 過程已成功完成。
下面把那個大表「弄髒」。因爲undo表空間很是小,因此但願儘量多地更新這個大表的塊,同時生成儘量少的undo。爲此,將使用一個UPDATE語句來執行該任務。實質上講,下面的子查詢要找出每一個塊上的「第一個」行rowid。這個子查詢會返回每個數據庫塊的一個rowid(標識了這個塊的一行)。咱們將更新這一行,設置一個VARCHAR2(1)字段。這樣咱們就能更新表中的全部塊(在這個例子中,塊數大約比8,000稍多一點),緩衝區緩存中將會充斥着必須寫出的髒塊(如今只有500個塊的空間)。仍然必須保證只能使用那個小undo表空間。爲作到這一點,並且不超過undo表空間的容量,下面構造一個UPDATE語句,它只更新每一個塊上的「第一行」。ROW_NUMBER()內置分析函數是這個操做中使用的一個工具;它把數字1指派給表中的數據庫塊的「第1行」,在這個塊上只會更新這一行:
scott@ORCL>update big 2 set temporary = temporary 3 where rowid in 4 ( 5 select r 6 from ( 7 select rowid r, row_number() over 8 (partition by dbms_rowid.rowid_block_number(rowid) order by rowid)rn 9 from big 10 ) 11 where rn = 1 12 ) 13 / 已更新11979行。 scott@ORCL>commit; 提交完成。
如今咱們知道磁盤上有大量髒塊。咱們已經寫出了一些,可是沒有足夠的空間把它們都放下。接下來打開一個遊標,可是還沒有獲取任何數據行。打開遊標時,結果集是預約的,因此即便Oracle並無具體處理一行數據,打開結果集這個動做自己就肯定了「一致時間點」,即結果集必須相對於那個時間點一致。如今要獲取剛剛更新並提交的數據,並且咱們知道沒有別人修改這個數據,如今應該能獲取這些數據行而不須要如何undo。可是此時就會「冒出」延遲塊清除。修改這些塊的事務太新了,因此Oracle必須驗證在咱們開始以前這個事務是否已經提交,若是這個信息(也存儲在undo表空間中)被覆蓋,查詢就會失敗。如下打開了遊標:
scott@ORCL>variable x refcursor scott@ORCL>exec open :x for select * from big; PL/SQL 過程已成功完成。
啓動9個SQL*Plus會話
每一個會話運行的腳本以下:
begin for i in 1 .. 1000 loop update small set y = i where x= &1; commit; end loop; end; /
這樣一來,就有了9個會話分別在一個循環中啓動多個事務。咱們觀察到下面的結果:
ERROR: ORA-01555: snapshot too old: rollback segment number 23 with name "_SYSSMU23$" too small no rows selected
它須要許多條件,全部這些條件必須同時存在纔會出現這種狀況。首先要有須要清理的塊,收集統計信息的DBMS_STATS調用就能消除這種塊。儘管大批量的更新和大量加載是形成塊清除最多見的理由,可是利用DBMS_STATS調用的話,這些操做就再也不成爲問題,由於在這種操做以後總要對錶執行分析。大多數事務只會接觸不多的塊,而不到塊緩衝區緩存的10%;所以,它們不會生成須要清理的塊。萬一你發現遭遇了這個問題,即選擇(SELECT)一個表時(沒有應用其餘DML操做)出現了ORA-01555錯誤,能你能夠試試如下解決方案:
1. 首先,保證使用的事務「大小適當」。確保沒有沒必要要地過於頻繁地提交。
2. 使用DBMS_STATS掃描相關的對象,加載以後完成這些對象的清理。因爲塊清除是極大量的UPDATE或INSERT形成的,因此頗有必要這樣作。
3. 容許undo表空間擴大,爲之留出擴展的空間,並增長undo保持時間。這樣在長時間運行查詢期間,undo段事務表中的事務槽被覆蓋的可能性就會下降。針對致使ORA-01555錯誤的另外一個緣由(undo段過小),也一樣能夠採用這個解決方案(這兩個緣由有緊密的關係;塊清除問題就是由於處理查詢期間遇到了undo段重用,而undo段大小正是重用undo段的一個根本緣由)。實際上,若是把undo表空間設置爲一次自動擴展1MB,並且undo保持時間爲900秒,再運行前面的例子,對錶BIG的查詢就能成功地完成了。
4. 減小查詢的運行時間(調優)。