天天看點

Oracle核心技術揭密. 1.2 段中塊的使用

<b>1.2 段中塊的使用</b>

在講解本節主題前,我們先來理清一個概念,什麼是段。在oracle中,表和段是兩個截然不同的概念。表從邏輯上說明表的形式,比如表有幾列,每列的類型、長度,這些資訊都屬于表。而段隻代表存儲空間,比如,上節中提到的區,就是屬于段。一個段中至少要包含一個區。

oracle中,每個對象都有一個id值,表有表的id,段有段的id。在dba_objects資料字典視圖中,object_id列是表id,data_object_id列是段id,下面檢視了某個表的表id和段id:

sql&gt; create table lhb.table_lhb2 (id

int,name varchar2(20)) tablespace tbs_ts2;

table created.

sql&gt; select object_id,data_object_id

from dba_objects where owner='lhb' and

 object_name='table_lhb2';

 object_id data_object_id

---------- --------------

 13039    

13039

從上面資訊可知,這裡建立了一個表table_lhb2,初始情況下,它的表id和段id是一樣的,都是13039。

表id一旦建立,就不會再改變。但段id是會變化的,比如,當truncate表時,oracle會将表原來的段删除,再為表建立一個段。也就是将表原來的存儲空間釋放,再重新配置設定新的區。這個過程完畢後,表就換了一個段,是以,表id不變,但段id卻變了。如下所示:

sql&gt; insert into lhb.table_lhb2

values(1,'abc');

1 row created.

sql&gt; commit;

commit complete.

sql&gt; truncate table lhb.table_lhb2;

table truncated.

object_name='table_lhb2';

13040

可以看到,在truncate表後,object_id不變,data_object_id變了。基本上,每truncate一次,段id都會加1。

注意,上面的測試是在oracle 11gr2中做的,如果是在oracle 10g中,建立表後不需要插入一行,直接truncate,就可以觀察到段id的變化。

<b>1.2.1 塊中空間的使用</b>

一個塊的大小最常見是8kb。對于這8kb空間的使用,網上已經有很多描述,這裡簡單說一下。塊中資訊分兩部分:管理資訊和使用者資料,其中,管理資訊包括塊頭的scn、itl槽等。

塊的結構相信很多人也研究過,下面讨論一個常見問題:如果删除了一行,再復原,行的位置會變嗎?

測試如下:

sql&gt; select

dbms_rowid.rowid_relative_fno(rowid) fno,dbms_rowid.rowid_block_

number(rowid)

block_id,dbms_rowid.rowid_row_number(rowid) row_id,id from lhb.

table_lhb2;

fno   block_id     row_id         id

---------- ---------- ---------- ----------

         5       

517          0          1

5        517          12&lt;---(删除此行再復原)

5        517          23

5        517          34

這裡使用了一個包,dbms_rowid,它的作用是從rowid中将對象id、檔案号、塊号、行号分解出來。或者把對象id、檔案号、塊号、行号合并成rowid,具體使用方法這裡不再列出,可以參考oracle官方文檔pl/sql reference,其中有詳細的說明。這裡,使用它的第一種功能,從rowid中解析出塊号、行号等資訊。如果向lhb.table_lhb2表中依次插入id為1、2、3、4的4行資料,觀察row_id列,可以看到,這4行的行編号分别是0、1、2、3。

下面将id為2的行(行編号是1)删除,再復原,然後再次檢視。

sql&gt; delete lhb.table_lhb2 where id=2;

1 row deleted.

sql&gt; rollback;

rollback complete.

5        517          0          1

5        517          12&lt;---(復原後行号不變)

結果不變,id為2的行,還是在行号為1的位置。

道理很簡單,删除某行,其實隻是在行上加個删除标志,聲明此行所占的空間可以被覆寫。在沒有送出時,事務加在行上的鎖并沒有釋放,此行雖然已經打上了删除标志,但空間仍不會被其他行覆寫。而删除行的復原,其實就是将被删除的行重新插入一次。但復原時的插入和普通插入一行還是有一定差別的。因為被删除行的空間不會被覆寫,是以復原時的插入,不需進行尋找空間的操作,而是行原來在哪兒,就還插入到那裡。這也就是它和普通插入的差別。

是以,删除的復原,不會改變行原來的位置。

但如果删除後送出再插入呢?行的位置肯定就會發生變化了。

<b>1.2.2 典型問題:堆表是有序的嗎</b>

曾經有位開發人員跟我聊到,他曾做過測試,插入幾萬行,删掉,再插入,發現原來oracle中堆表是按插入順序安排行的位置的,而且這個測試他做了好多遍,都是這個結果。現在他們有個應用,顯示資料時,要求先插入的行在前,後插入的行在後,其實oracle已經幫他們實作了這個功能。

事實上,堆表是無序的,堆表的特點就是無序、插入快速。

oracle在插入行時是如何在資料塊内查找可用空間的呢?這有點類似于上節中提到的區的配置設定過程。oracle會在資料塊中設立一個标記位,記錄空間使用到哪兒了。

塊中使用者資料所占空間是從下往上配置設定的。假設,在8192位元組的塊中插入了5行,每行100位元組,也就是說,空間已經使用到了(8192-500)7692位元組處,那麼,标記位的值就是7692。

如果删除了其中一行并送出,标記位的值不會變,還是7692。再重新插入被删除行,或插入新行,将會從7692處向上查找可用空間,删除行釋放出的空間不會被使用。

當标記位的值越來越小,向上到達管理性資訊的邊界時,标記位會再變為8192。

我們可以測試一下。

sql&gt;commit;

sql&gt; insert into lhb.table_lhb2 values

(2,'abc');

5        517          2          3

5        517          3          4

5        517          4          2&lt;------删除送出後再插入,

                                                        被配置設定到了新的位置

在上面的測試中,先删除id為2的行,送出後接着又插入id為2的行。不過,新插入的行并沒有使用剛剛删除行的空間。

如果隻測試到這一步,很容易得出結論,行的位置就是插入順序。但别忘了,我們隻在一個塊内進行了觀察,查找了可用空間。在衆多的塊中,oracle是如何選擇要向哪個塊中插入的呢?情況會不會有變化呢?我們還不知道。

是以,現在還不能完全回答“堆表是有序的嗎”這個問題,繼續向下看,據說assm對插入的影響是巨大的,那接下來看看assm。

<b>1.2.3 assm與l3、l2、l1塊的意義</b>

assm的目的是大并發插入,這應該是dba要掌握的基本知識。在輸入輸出能力滿足的情況下,使用assm就能有大并發插入嗎?這可不一定。工具再好,還要看我們如何使用工具。

在了解assm的使用注意事項之前,先來分析一下assm。為什麼oracle對外宣稱assm可以支撐大并發插入應用呢?

assm的整體結構是3層位圖塊+資料塊,即共4層的樹狀結構。

第一層位圖塊稱為l3塊,一個l3塊中可以存放多個l2塊的位址,一個l2塊中可以存放多個l1塊位址,一個l1塊中可以存放多個資料塊位址,如圖1-2所示。

圖1-2 assm的整體結構

第一個l3塊一般是段頭。如果段頭中存放了太多l2塊的資訊,空間不足,oracle會再配置設定第二個l3塊。當然,段頭中會有第二個l3塊的位址。如果第二個l3塊空間也用完了,會再配置設定第三個。第二個l3塊中會存放第三個l3塊的位址。通常情況下,一個l3塊就夠了。有兩個l3塊就已經是非常罕有的情況了,基本上不會出現需要3個l3塊的情況。

oracle是如何使用4層樹狀結構(3層位圖塊+資料塊)來确定向哪個塊中插入的呢?

第一步,查找資料字典(就是dba_segments資料字典視圖的基表),确定段頭位置。

第二步,在段頭中找到第一個l2塊位置資訊。

第三步,到l2塊中根據執行插入操作程序的pid号,做hash運算,得到一個随機數n,在l2中,找到第n個l1塊的位置資訊。

第四步,到第三步中确定的l1塊中,再根據執行插入操作程序的pid号,做hash運算,得到一個随機數m,在l1中找到第m号資料塊。

第五步,向第m号資料塊中插入。

l3塊中雖然可以有多個l2塊,但插入操作不會選擇多個l2塊,每次隻會選擇同一個l2塊。直到這個l2塊下面的所有資料塊都被插滿了,才會選擇下一個l2塊。

在l2中選擇某個l1的時候,就是随機的了。不同session,隻要有可能,就會被配置設定到不同的l1中。在l1中找資料塊時也是一樣。

現在我們可以回答這個問題了:oracle為什麼宣稱assm可以支援大并發插入。

假設一個l2中有100個l1,每個l1中有64個資料塊,可以算一下,100×64,如果oracle的随機算法真的夠随機,如果有6400個程序一起執行插入操作,oracle會随機地将它們配置設定到6400個資料塊中。

oracle的随機算法一向都是值得信賴的。

是以,在oracle的所有資料中,都宣稱assm可以支撐大并發插入。

但實際情況往往不像想象中的這麼簡單。

<b>1.2.4 值得注意的案例:assm真的能提高插入并發量嗎</b>

這個案例很有代表性,如果不深入到細節中,很容易在中途得出錯誤的結論。下面較長的描述思考過程,希望能給大家帶來些啟發。

曾經遇到過這樣的應用,要求對使用者的登入、退出行為做記錄。此部分的邏輯很簡單,使用者每登入一次應用,向資料庫中一個日志表中插入一行,退出應用的時候再向日志表中插入一行。

此日志表是個日分區表,每天一個分區。每天大約會插入千萬行,除了插入并發很高以外,就沒有其他的大并發操作。另外,每天晚上會将當天的資料推送到資料倉庫,在資料倉庫中再進行分析、對比。

項目上線後,有些使用者反映登入變慢了。而且,隻有上午八九點鐘左右的時候慢,過了這一段時間就沒有使用者反映有問題。經過對比awr,發現變慢是不定時的,從8點開始,到9點左右為止,在半小時一次的報告中,偶爾會有那麼一兩份awr會顯示buffer busy waits比較高,然後就正常了。

看到這個情況,很容易讓人認為是某個時間段有很多人一起在通路同一張表,其他時間又不一起通路了。究竟是不是這麼回事呢?

先來确定一下等待是針對哪個對象。通過v$segment_statistics,查找statistic_name列為buffer busy waits的,或者,檢視v$active_session_history中的曆史等待事件,根據p1、p2列的值,就可以定位争用是針對哪個對象的。

根據檔案号、塊号查找的結果來看,絕大多數的buffer busy waits都出現在日志表上。

日志表每天分區的資料量最高接近千萬行,就按每天1000萬行算,除以3600×24,平均每秒116個并發插入。當然,還要考慮高低峰的問題,晚上應用基本上沒什麼人用的,這幾百萬行大部分都是白天插入的。是以,再乘個2,每秒232的插入量,這是最高的了。也并不是很多,這點量和oracle宣稱的assm支援的高并發插入相比,應該不會有buffer busy waits。

但無論如何,buffer busy waits是産生了,有可能以主機的硬體來論,現在已經是并發插入量的極限了。但奇怪的是,這種情況每天隻會在剛上班後不久(8~9點)出現,其他時段正常。

難道是剛上班時向日志表的插入量高?

但統計的結果顯示,白天有好幾個時段,日志表的插入量都很大,并不是早上上班時段特别大,有時下午還會比上午插入的稍多些,但沒有發現下午日志表上有buffer busy waits,下午也從來沒人反映過慢,而且整庫的壓力上下午基本差不多。

如果全天都有buffer busy waits,我想我也會放棄進一步調查。但有時下午的插入量多,反而沒有等待。那說明assm是足以支撐這個量級的并發插入的。想解決問題的話,第一步是定位問題,這我們都知道。可如何定位這個問題呢?

遇到這樣的疑難雜症,一般的方法是在測試環境中詳細地分析相關操作,甚至可以使用dtrace加mdb/gdb這種底層分析工具。總之,隻有清楚地了解底層操作,才能分析出問題在哪兒。

如何發現現在遇到的這個問題出在哪兒呢?

很簡單,還是從最基本的測試做起。先建一個表,驗證一下oracle插入時,是否會随機地選擇塊。如下所示:

sql&gt; drop tablespace tbs_ts1 including

contents;

tablespace dropped.

sql&gt; create tablespace tbs_ts1 datafile

'/u01/disk1/tbs_ts1_01.dbf' size 50m reuse

uniform size 1m;

tablespace created.

sql&gt; create table table1(id int,name

varchar2(20)) tablespace tbs_ts1;

由于線上環境表空間區大小是1mb,是以在測試環境,我也建立了個區大小為1mb的表空間。

在oracle 10g以後,oracle預設的表空間類型就是assm了,是以,不需要專門指定了。

接着,在tbs_ts1表空間中建立一個測試表table1,下面來看看它的區占用情況。

sql&gt; select extent_id, file_id,

block_id, blocks from dba_extents where

owner='lhb' and segment_name='table1' order

by extent_id;

 extent_id   

file_id   block_id     blocks

0          4        128        128

可以看到,table1在4号檔案中,第一個區開始自128号塊處。可以dump一下128号塊看看,它是一個l1塊。129号塊也是一個l1塊,130号塊是l2塊,131号塊是段頭,也是l3塊。

128号和129号塊中,各自有64個資料塊資訊。這一點,可以通過dump來确認。

下面,插入一行,試試看這一行将被插入哪個塊中。

sql&gt; insert into table1

values(1,'aaaaaa');

block_id,dbms_rowid.rowid_row_number(rowid) row_id,id from lhb.table1;

4        155          0          1

在插入這一行并送出後,可以用之前介紹過的語句,檢視這一行的位置。可以看到,它被插入在了4号檔案155号塊中。換個會話再插入一行試試。

values(2,'bbbbbb');

4        156          0          2

在另一個會話中,插入了id為2的行,它被插入在了156号塊中。

不同的會話,oracle會将行插入到不同塊中。oracle是根據pid計算出的随機數,随機地将行插入在不同的塊中。隻要pid不一樣,行就會被插入在不同的塊中。在pid一樣的情況下,行會被插入在同一塊中。

比如,在第一個會話中再插入一個id為3的行。

values(3,'aaaaaa');

4        155          1          3

id為3的行也被插入到155号塊中。因為它和id為1的行是在同一會話中插入的,會話對應程序的pid相同,兩行就被插入了同一個塊中。

另外,我們可以發現,後插入的id為3的行,在顯示時被排在先插入的id為2的行前了。這說明堆表中行的排列也并非是插入順序。

現在我們終于可以對前面提出的“堆表是有序的嗎”問題給出一個明确的回複了。那就是:完全無序。因為插入時有個根據pid計算随機數的過程,這就會導緻行被插入哪個塊是随機的。是以,堆表是無序的。

繼續觀察行被插入的位置。但如果我們老是通過sqlplus lhb/a建立一個會話,在會話中插入,這樣太麻煩了,還是寫個腳本吧。

$ cat assm_test.sh

sqlplus lhb/a &lt;&lt;eof

insert into lhb.table1

values($1,'aaabbbcccd');

commit;

exec dbms_lock.sleep(10000);

eof

關于shell腳本的編寫,這裡不再解釋。下面隻說一點,為什麼最後要加如下語句:

如果沒有這個暫停操作,會話将立即結束。在oracle中,如果前一個會話結束,下一個會話馬上建立,則下一個會話将會有和前一個會話相同的session id和pid(注意,pid不是spid,pid是oracle對程序的編号)。如果兩個會話的pid相同,行将被插入在同一塊中。是以,這裡專門加個“暫停”操作,讓會話停10000秒後再退出。這樣,再建立一個會話,它将有一個新的pid。

按如下方法,将上述腳本執行10次:

./assm_test.sh 4&amp;

./assm_test.sh 5&amp;

加一個&amp;,表示放在背景執行,要不然要等10000秒才能結束。

檢視一下這些行都被插到哪兒了。

dbms_rowid.rowid_relative_fno(rowid) fno,dbms_rowid.rowid_block_number

(rowid)

4        157          0          4

4        158          0          5

4        159          0          6

4        160          0          8

4        161          0          7

4        162          0          9

4        163          0         10

4        164          0         11

4        165          0         13

4        166          0         12

13 rows selected.

還是很平均的,每個塊一行。我們看一下row_id列,這是行在塊中的行号。除了剛才做測試的id為3的行,其他行都是塊中的第一行(行号為0)。

平均是很平均,但我們應該也注意到了一個問題,在後面所做的10次插入,雖然這10行的确被插到了10個塊中,但是,這些塊未免有點太集中了。

table1表現在共有128個塊,塊編号從128到255。但這些行都被插到了155~166号塊中。

這應該是oracle的算法不夠随機吧。

一開始我覺得,是區不夠多,隻有一個區,128個塊,oracle選擇面太窄了。我們知道,表在擴充時,也都是一個區一個區地擴充的。每次占滿了128個塊後,再擴充下一個區。但下一個區也還是128塊,還是隻在128個塊中選擇。由于随機算法不夠随機,導緻在128選一時,很多行被同時插到了同一個塊中,這時,就會出現buffer busy waits。

一切都是合乎情理,我馬上将發現告知應用方。解決方案就是,在晚上資料庫空閑時,為日志表手動分很多個區。

第二天,客戶依然反映,運作速度慢。檢視資料庫,還是有buffer busy waits。

為什麼?

看來是第一次的實驗做得不夠徹底。為什麼使用的塊是155号、156号、157号等,這麼有順序,而且不夠分散呢?

繼續前面的測試。這次,我調用./assm_test.sh n&amp;,每10次觀察一下行的配置設定情況,終于發現了問題。

sql&gt; select dbms_rowid.rowid_relative_fno(rowid)

fno,dbms_rowid.rowid_block_number

4        132          0        

39

                   ……………………

4        159          1         67

4        160          1         66

4        161          1         68

4        162          1         69

4        190          0         38

4        191          0         37

69 rows selected.

一共插入了69行,最小的塊号是132。這個可以了解,因為表的第一個塊編号是128,128号、129号塊是l1,130号是l2,131号是段頭兼l3。第一個可用資料塊是從132開始的。但是到150号塊後,就開始有重複,兩行被插入同一塊中。還有一點就是,最大使用的塊是191号。用192-128,正好等于64。

繼續分析下去有個關鍵點,要看之前對細節的挖掘程度了。前面我們一再地提過,對于1mb大小的區,每個區最前面的兩個塊,大多數情況下是l1塊。在8kb的塊大小下,1mb共128個塊,兩個l1,正好每個l1記錄64個資料塊。

好了,答案基本上已經浮出水面。

oracle隻使用了第一個l1塊中的資料塊,而沒有使用第二個l1中的塊。

其實還有一個知識點,如果不具備,可能分析就到這裡為止了。前面也提過了,oracle在l3、l2、l1、資料塊中這個樹狀圖中選擇要插入的塊時,從l3中選擇l2并不是随機的,每次都隻選某一個。但從l2中選擇l1是随機的。關于這一點,我已經做了測試。

現在l2中有兩個l1,會什麼oracle隻選擇第一個l1呢?

你想到原因了嗎?

我是這樣想到原因的,我曾經做過直接路徑插入的測試,這個測試驗證了如果進行直接路徑插入,每次會在高水點之上配置設定空間,如果送出,則修改高水點。如果不送出,則不修改高水點,通過這種方式可減少undo的耗用。而普通的插入則是在高水點之下尋找空間。

我們一直沒有提過高水點。直接路徑都是在高水點之上插入的,那麼間接路徑呢?肯定是在高水點之下了。

好,答案已經見分曉了。高水點肯定在第192号塊。因為第二個l1塊中的資料塊,都在高水點之上,是以,第二個l1塊中的資料塊不會被插入算法選擇到。

dump一下段頭驗證一下吧。

extent control header

---------------------------------------------------------------

extent header:: spare1: 0      spare2: 0     #extents: 1      #blocks: 128  

last map        0x00000000     #maps: 0      offset: 2716 

highwater::     0x010000c0      ext#: 0      blk#: 64         ext size: 128 

代碼中加下劃線的就是高水點了。0x010000c0,這個是dba(data block address,資料塊位址)。它的前10個二進制位是檔案号,後面的是塊号。0x010000c0也就是4号檔案192号塊。

看來oracle的高水點每次向後移動時,是以l1塊中的資料塊數量為機關的啊。

水落石出了,原來是高水點太低的問題。

oracle隻告訴我們,assm可以增大插入并發量,但沒告訴我們,并發插入量還要受高水點限制。

以前曾經有人讨論過在mssm表空間中高水點的移動規則,而assm下高水點的推移規則還很少有人注意過。

當在區中插入第一行時,高水點移到區的第一個l1塊中最大的資料塊後。這句話有點繞,還是以我們的測試表table1為例吧:插入第一行時,高水點移到了第一個l1塊(128号塊)中最大的資料塊後,128号塊中最大的資料塊是191,那麼高水點就是192了,其實也就是第二個l1塊中的第一個資料塊。

簡單總結一下,高水點的移動,在assm下,是以l1中資料塊的數量為準的。

如果塊大小是8kb,區大小是1mb,l1中有64個資料塊,高水點就是以64個塊為機關,依次往後挪的。也就是說,我們的并發插入,每次都隻是向64個塊中插入。可以想象,如果同時有100個程序插入,但隻有64個塊接收,将有36個程序不得不和另一個程序同時向一個塊中插入。

兩個程序同時修改一個塊,會有什麼等待時間呢?buffer busy waits(當然也會有少量的cache buffer chain latch)。

問題已經找到一大半了,assm表空間仍有可能因為高水點不高,可用于插入的塊不多,造成buffer busy waits。但另一半問題隐藏得更深,為什麼隻會在剛上班那會兒出現這個等待,而其他時間則沒有呢?

注意,白天的時候,壓力是差不多的。有時下午比上午還要高。

要解答這個問題,就看你對oracle的内部機制有多大的好奇心了。

我挖掘出這個問題純屬意外。

其實在發現了高水點問題後,我建議使用擡高高水點的方式解決争用問題。

當然,擡高高水點後,将對全表掃描不利。全表掃描隻掃描高水點之下的塊,如果高水點太高,要掃描的塊也多了。

但這個日志型應用,平常沒有全表掃描,隻有在每天晚上向資料倉庫傳資料時,需要全表掃描。是以,對全表掃描的影響不是主要考慮的因素。

如何擡高呢?手動配置設定區是無法擡高高水點的。隻有一種方法,先插入行再删除。

因為日志表是一個日分區表,按照日期,每天一個分區。考慮到每天的插入量不會高于1000萬行,是以決定對未來的每個分區,先插入1000萬行,再用delete删除。

具體的方案是這樣的,先使用append向一張中間表中插入1000萬行,采用直接路徑方式,這樣産生的undo量較少。再用delete慢慢删除,根據rowid來删除,一次删除一個區的所有行,然後送出。将整個表删除完後,高水點就已經被擡高了,但表中是沒有行的。再使用分區交換指令,将被擡高高水點的中間表交換到日志表中。

這種方法聽起來有點不太規範,但沒辦法,暫時隻能這樣解決了。

實事上,我用上面的方式調高了幾個分區的高水點,第二天觀察,果然在全天任意時候,都不再有buffer busy waits了。

其實如此交差也可以,就是加分區的時候麻煩點。若用腳本實作,隻是在delete的時候慢點,不占太多復原段就不會有任何問題。

但還有一個問題一直困擾着我,但這個問題和應用已經無關了,我隻是好奇:一個l1中有64個資料塊,64這個數字是固定的嗎?

我分别用40kb(5個8k的塊,已經是oracle中最小的區了)、1mb、10mb、30mb大小的區測試,40kb的區中,一個l1中可以隻有5個資料塊,是最少的。但1mb、10mb、30mb的區,都是一個l1中有64個塊。64個塊應該就是l1中資料塊的最大數量了。

oracle的系統管理區大小是随着段的不斷變大而不斷變大的,l1會不會也是這樣呢?我決定再試一下。

用手動配置設定區的指令,為table1多配置設定些區。我為table1每次配置設定30mb空間,每次dump一下最後一個區的第一個塊(每個區第一個塊通常都是l1塊)。

當配置設定的總空間到90mb時,我發現l1中的資料塊數量從64增加到了256個。測試如下:

'/u01/disk1/tbs_ts1_01.dbf' size 100m

 =reuse uniform size 1m;

sql&gt; alter table table1 allocate extent

(size 90m);

table altered.

sql&gt; set pagesize 1000

1          4        256        128

2          4        384       

128

…………………………

88          4      11392        128

89          4      11520        128

90          4      11648        128

91 rows selected.

上面删除了表空間,重建立了個全空的,區大小1mb。又建立了個新表,table1,手動配置設定90mb空間。它一共有91個區。

分别dump一下第128号塊和11520号塊。以下是4号檔案128号塊的dump結果:

mapblk 

0x00000000  offset: 0    

--------------------------------------------------------

  dba

ranges :

0x01000080 

length: 64     offset: 0     

0:metadata        1:metadata       2:metadata       3:metadata

4:unformatted    

5:unformatted   

6:unformatted    7:unformatted

8:unformatted    

9:unformatted  

10:unformatted     ………………

         ……………………

60:unformatted  

61:unformatted   62:unformatted   63:unformatted

可以看到,這個l1中共有64個資料塊。以下是11520号塊的dump結果:

0x01002d00 

length: 128    offset: 0     

0x01002d80 

length: 128    offset: 128

0:metadata         

1:unformatted    

2:unformatted     3:unformatted

4:unformatted      

5:unformatted    

6:unformatted     7:unformatted

252:unformatted   253:unformatted   254:unformatted   255:unformatted

在這個l1塊中,資料塊的數量增加到了256個。

這證明了l1塊中記錄的資料塊個數也是随着表的不斷增大而增多的。

這個證明有何意義呢?還記得上面遇到的問題吧,每天總是在剛上班時會有buffer busy waits,而其他時間則沒有。現在有答案了。

因為日志表每天一個分區,也就是每天一個段。剛上班時,段還比較小,l1塊中隻有64個資料塊,是以并發插入每次都隻針對64個塊。随着表增大,當表超過90mb時,一個l1就有256個資料塊了,即使所有并發都隻針對一個l1中的資料塊,256個塊也足以支撐這套應用的所有并發了。是以,每天總是在最開始不長一段的時間内,會有buffer busy waits,再往後就正常了。

這個奇怪的問題終于找到了原因。其實我研究l1中資料塊的數量,本來隻是為了滿足好奇心,沒想到可以查找出這個問題的原因。

更進一步,可以再試一下不同區大小、不同段大小下,l1塊中資料塊的數量。

我測試的結果是,10mb區大小,從第4個區開始,l1塊中資料塊的數量就已經是256個了。10mb的區好像有點大了,我隻測試了一下4mb或8mb的區,在段大小超過64mb後,l1塊中資料塊的數量會達到256個。

好,研究得差不多了。可問題該如何解決呢?方法還和剛才一樣,先插入,再删除。隻不過,不需要插入1000萬行了。我選擇建立8mb區大小的表空間,日志表新的分區都建到新表空間中。每個分區隻需插入50萬行再删除就可以了。

隻需要将前8個分區,插入滿行,再删除,将高水點推到第8個分區後,因為第8個分區後,每個l1塊中都是256個資料塊,足夠支撐并發插入量了。

該問題終于有了一個比較好的解決方案。但後面經過觀察又發現,在l1塊上出現了争用,但不嚴重,沒有造成反應延遲。oracle的高水點每次以l1塊中資料塊的量為機關向後擴,始絡會有問題。如果同一時刻的并發超過了256個,一樣會有争用,而且,這麼大的量,l1塊的競争也會大大加劇。這樣的話,解決方法隻有一個,就是像我最初的方案一樣,插入很多行(比如1000萬行),将高水點拉得很高,再删除。

好了,assm的問題就說到這兒。看來随便建個assm表空間,再建個表上去,就想支撐大并發插入,這種想法有點簡單了。

實際案例就先介紹到這兒。希望通過這個案例讀者能有所收獲。

補充一句:對oracle越熟悉,面臨的疑難雜症就越少。

關于表空間和存儲結構,還有兩個疑問:全表掃描時,oracle是如何找到表的塊在哪兒的?索引掃描oracle是如何找到root塊的?

<b>1.2.5 段頭與extent map</b>

上一節提到了,段頭是第一個l3塊,就是說段頭中包含l3資訊。其實,段頭中的重要資訊,除了l3外,還有extent map,将其直譯過來就是區地圖。

顧名思義,區地圖就是記錄一個段中所有區都在哪兒的地圖。全表掃描操作,就是按圖索骥,按區地圖逐個讀取所有區。

讓我們來看看區地圖是什麼樣子,同時,也模拟一下全表掃描的執行流程。

第一步,确定段頭位置。

sql&gt; select header_file ,header_block

from dba_segments where segment_name='table1';

header_file header_block

----------- ------------

          4         

131

當然,oracle肯定不會讀dba_segments這個資料字典視圖,它會讀dba_segments低層seg$這樣的資料字典表。會先到共享池中的字典緩存中查找seg$相關的行,如果沒有找到,再到buffer cache中讀seg$相關的塊,如果還沒有,就到磁盤上system表空間中讀seg$表。

當找到table1的段頭位置時,oracle會讀取它裡面的區地圖,我們來dump一下。

執行下面的指令dump:

exit

sqlplus / as sysdba

alter system dump datafile 4 block 131;

就是先退出sqlplus,再重新連接配接,然後去dump。因為同一伺服器程序會把dump資訊寫到一個dump檔案中。如果你dump多次,被會寫進一個檔案,這樣觀察起來不友善。我退出再登入,伺服器程序會換一個的,spid也會不同,這樣dump資訊會被寫到不同的檔案中,便于檢視。

下面就是段頭中的區地圖資訊:

extent map

-----------------------------------------------------------------

length: 128  

0x01000100 

0x01000180 

…………

length: 128

第一個區,開始自0x01000080處,前10個二進制位是檔案号,後面是塊号,前面已經提到過的,也就是4号檔案128号塊處。這個區的大小是128個塊,最後一個區,開始自4号檔案11648号塊處(就是最後一行0x01002d80),大小也是128個塊。

我們已經看到區地圖了,很簡單是吧?但全表掃描時oracle讀取的并不是這裡的區地圖,還要往下看。

auxillary map

 extent 0     :  l1

dba:  0x01000080 data dba:  0x01000084

 extent 1    

:  l1 dba:  0x01000100 data dba:  0x01000102

 extent 2    

:  l1 dba:  0x01000180 data dba:  0x01000182

                   ………………

 extent 89   

:  l1 dba:  0x01002d00 data dba:  0x01002d01

 extent 90   

:  l1 dba:  0x01002d00 data dba:  0x01002d80

在上面的資訊中,出現了auxillary map,直譯過來是輔助地圖。這一部分資訊更詳細。l1 dba:  0x01000080,說明了此區内第一個l1塊開始的地方,即4号檔案的128号塊。data

dba:  0x01000084,說明使用者資料開始的地方,即132号塊。這裡說明了真正的使用者資料開始自哪裡,oracle全掃描時,是按照“data dba:*******”後的dba查找區的。但這裡沒有區長度,是以,上面那部分區地圖資訊還是要讀的。

另外,我們看最後兩行:

extent 89   

extent 90   

這兩行的l1 dba一樣,都是0x01002d00,即4号檔案11520号塊。為什麼這樣?因為11520号塊中有256個資料塊,是以這兩個區隻需要有一個l1塊就行了。可以觀察一下從什麼地方開始兩個區隻要一個l1塊,這裡是從8192号塊開始的。

extent 61   

:  l1 dba:  0x01001f00 data dba:  0x01001f02

extent 62   

:  l1 dba:  0x01001f80 data dba:  0x01001f82

extent 63   

:  l1 dba:  0x01002000 data dba:  0x01002001

extent 64   

:  l1 dba:  0x01002000 data dba:  0x01002080

可以看到,61号、62号區,還各自有不同的l1号塊,而63号、64号區,已經隻有63區頭的一個l1塊了。63号區也就是第64個區,每個區1mb,也就是當段大小超過64mb時,一個l1将放存256個資料塊。

好了,這就是區地圖,通過研究它,全表掃描操作的流程我們應該也都清楚了。很簡單,找到段頭,讀取區地圖資訊,根據區地圖的順序,讀取每一個區。是以,全表掃描的顯示順序,就是區地圖中區的順序,其實也就是dba_extents中區的順序。

下面再來看一下全表掃描的邏輯讀。

sql&gt; drop tablespace tbs_ts2 including

sql&gt; create tablespace tbs_ts2 datafile

'/u01/disk1/tbs_ts2_01.dbf' size 20m reuse

uniform size 40k;

sql&gt; drop table table2;

table dropped.

sql&gt; create table table2(id int,name

varchar2(20)) tablespace tbs_ts2;

sql&gt; insert into table2 values(1,'abc');

commit complete

sql&gt; set autot trace

sql&gt; select * from table2;

……………………

statistics

----------------------------------------------------------

recursive calls

0  db

block gets

consistent gets

physical reads

redo size

594 

bytes sent via sql*net to client

520 

bytes received via sql*net from client

sql*net roundtrips to/from client

sorts (memory)

sorts (disk)

rows processed

注意,做觀察邏輯讀的測試時,對測試sql語句select * from table2,要反複多次執行。

這裡重新建立了一個tbs_ts2表空間,它的區大小隻有40kb,也就是5個塊。然後建了一個表,随便插入一行,插入的這一行将會使高水點被擡升到區的最後一個塊之後。

這個區隻有5個塊,前三個塊分别是l1、l2和段頭,可以存放使用者資料的隻有第4、5兩個塊,那麼高水點将在第5個塊之後。

為什麼邏輯讀是4次呢?全表掃描,要跳過l1、l2,隻讀段頭和高水點下的所有塊,也就是讀段頭和第4、5個塊。但是段頭要讀兩次,是以,邏輯讀為4。至于段頭讀兩次的原因,根據前面dump的段頭來看,段頭中的extent map、auxillary map資訊是分開存放的,要一次讀extent map,一次讀auxillary map,是以就要讀兩次了。

如何确定段頭讀兩次的問題呢?oracle 10g以前的版本,可以觀察latch的gets次數,但在11gr2後,就隻有使用dtrace跟蹤才能知道了。本書後面章節會有些這方面的内容,我們會逐漸深入到oracle内部,揭開oracle之謎。

<b>1.2.6 索引範圍掃描的操作流程</b>

索引範圍掃描,網上已經有很多讨論了,就是按照根、枝、葉的順序讀取。葉塊的位址在枝塊,枝塊位址在根塊。找到枝塊就可以找到葉塊,找到根塊就可以找到枝塊。那麼,如何找到根塊呢?

其實很簡單,在oracle中,根塊永遠在索引段頭的下一個塊處。是以,索引掃描是不必讀取索引段頭的。先在資料字典表中找到段頭位置,塊号加1就是根塊位置了。

對索引範圍掃描時的邏輯讀,可以做如下測試:

sql&gt; insert into table1 select

rownum,'abcde' from dba_objects;

12691 rows created.

sql&gt; create index table1_id on

table1(id) tablespace tbs_ts1;

index created.

sql&gt; exec

dbms_stats.gather_table_stats('lhb','table1');

pl/sql procedure successfully completed.

sql&gt; select blevel from dba_indexes

where index_name='table1_id' and owner='lhb';

blevel

----------

1

上面先向表中插入了10000多行,再建立了一個1層高的索引,索引隻有root塊和葉塊。

下面看看索引通路一次的邏輯讀:

sql&gt; select * from table1 where id=10;

596 

将測試sql語句select * from table1 where id=10多執行幾次,觀察到的邏輯讀為4。這4次邏輯讀分别是:root塊一次,葉塊兩次,資料塊一次。

葉塊之是以需要兩次,是因為索引是非唯一的。第一次讀葉塊是為了取出目标行rowid,第二次讀葉塊是判斷此葉塊中還有沒有滿足條件的行。

如果建成了唯一索引,不需要判斷葉塊是否還有滿足條件的行,葉塊就隻需要讀一次,一共隻需要3次邏輯讀。

sql&gt; drop index table1_id ;

index dropped.

sql&gt; create unique index table1_id on

460 

509 

表空間和存儲結構這就說到這兒。本章中的例子,都是在oracle 11gr2中做的,在oracle 10g中做同樣例子時的注意事項也都随例子說明了。

另外,本章的測試都是以8kb塊大小為例的,其他塊大小下的情況,留給讀者親自動手測試。