<b>1.2 段中塊的使用</b>
在講解本節主題前,我們先來理清一個概念,什麼是段。在oracle中,表和段是兩個截然不同的概念。表從邏輯上說明表的形式,比如表有幾列,每列的類型、長度,這些資訊都屬于表。而段隻代表存儲空間,比如,上節中提到的區,就是屬于段。一個段中至少要包含一個區。
oracle中,每個對象都有一個id值,表有表的id,段有段的id。在dba_objects資料字典視圖中,object_id列是表id,data_object_id列是段id,下面檢視了某個表的表id和段id:
sql> create table lhb.table_lhb2 (id
int,name varchar2(20)) tablespace tbs_ts2;
table created.
sql> 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> insert into lhb.table_lhb2
values(1,'abc');
1 row created.
sql> commit;
commit complete.
sql> 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> 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<---(删除此行再復原)
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> delete lhb.table_lhb2 where id=2;
1 row deleted.
sql> rollback;
rollback complete.
5 517 0 1
5 517 12<---(復原後行号不變)
結果不變,id為2的行,還是在行号為1的位置。
道理很簡單,删除某行,其實隻是在行上加個删除标志,聲明此行所占的空間可以被覆寫。在沒有送出時,事務加在行上的鎖并沒有釋放,此行雖然已經打上了删除标志,但空間仍不會被其他行覆寫。而删除行的復原,其實就是将被删除的行重新插入一次。但復原時的插入和普通插入一行還是有一定差別的。因為被删除行的空間不會被覆寫,是以復原時的插入,不需進行尋找空間的操作,而是行原來在哪兒,就還插入到那裡。這也就是它和普通插入的差別。
是以,删除的復原,不會改變行原來的位置。
但如果删除後送出再插入呢?行的位置肯定就會發生變化了。
<b>1.2.2 典型問題:堆表是有序的嗎</b>
曾經有位開發人員跟我聊到,他曾做過測試,插入幾萬行,删掉,再插入,發現原來oracle中堆表是按插入順序安排行的位置的,而且這個測試他做了好多遍,都是這個結果。現在他們有個應用,顯示資料時,要求先插入的行在前,後插入的行在後,其實oracle已經幫他們實作了這個功能。
事實上,堆表是無序的,堆表的特點就是無序、插入快速。
oracle在插入行時是如何在資料塊内查找可用空間的呢?這有點類似于上節中提到的區的配置設定過程。oracle會在資料塊中設立一個标記位,記錄空間使用到哪兒了。
塊中使用者資料所占空間是從下往上配置設定的。假設,在8192位元組的塊中插入了5行,每行100位元組,也就是說,空間已經使用到了(8192-500)7692位元組處,那麼,标記位的值就是7692。
如果删除了其中一行并送出,标記位的值不會變,還是7692。再重新插入被删除行,或插入新行,将會從7692處向上查找可用空間,删除行釋放出的空間不會被使用。
當标記位的值越來越小,向上到達管理性資訊的邊界時,标記位會再變為8192。
我們可以測試一下。
sql>commit;
sql> insert into lhb.table_lhb2 values
(2,'abc');
5 517 2 3
5 517 3 4
5 517 4 2<------删除送出後再插入,
被配置設定到了新的位置
在上面的測試中,先删除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> drop tablespace tbs_ts1 including
contents;
tablespace dropped.
sql> create tablespace tbs_ts1 datafile
'/u01/disk1/tbs_ts1_01.dbf' size 50m reuse
uniform size 1m;
tablespace created.
sql> create table table1(id int,name
varchar2(20)) tablespace tbs_ts1;
由于線上環境表空間區大小是1mb,是以在測試環境,我也建立了個區大小為1mb的表空間。
在oracle 10g以後,oracle預設的表空間類型就是assm了,是以,不需要專門指定了。
接着,在tbs_ts1表空間中建立一個測試表table1,下面來看看它的區占用情況。
sql> 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> 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 <<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&
./assm_test.sh 5&
…
加一個&,表示放在背景執行,要不然要等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&,每10次觀察一下行的配置設定情況,終于發現了問題。
sql> 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> alter table table1 allocate extent
(size 90m);
table altered.
sql> 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> 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> drop tablespace tbs_ts2 including
sql> create tablespace tbs_ts2 datafile
'/u01/disk1/tbs_ts2_01.dbf' size 20m reuse
uniform size 40k;
sql> drop table table2;
table dropped.
sql> create table table2(id int,name
varchar2(20)) tablespace tbs_ts2;
sql> insert into table2 values(1,'abc');
commit complete
sql> set autot trace
sql> select * from table2;
……………………
statistics
----------------------------------------------------------
recursive calls
0 db
block gets
4
consistent gets
physical reads
redo size
594
bytes sent via sql*net to client
520
bytes received via sql*net from client
2
sql*net roundtrips to/from client
sorts (memory)
sorts (disk)
1
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> insert into table1 select
rownum,'abcde' from dba_objects;
12691 rows created.
sql> create index table1_id on
table1(id) tablespace tbs_ts1;
index created.
sql> exec
dbms_stats.gather_table_stats('lhb','table1');
pl/sql procedure successfully completed.
sql> select blevel from dba_indexes
where index_name='table1_id' and owner='lhb';
blevel
----------
1
上面先向表中插入了10000多行,再建立了一個1層高的索引,索引隻有root塊和葉塊。
下面看看索引通路一次的邏輯讀:
sql> select * from table1 where id=10;
596
将測試sql語句select * from table1 where id=10多執行幾次,觀察到的邏輯讀為4。這4次邏輯讀分别是:root塊一次,葉塊兩次,資料塊一次。
葉塊之是以需要兩次,是因為索引是非唯一的。第一次讀葉塊是為了取出目标行rowid,第二次讀葉塊是判斷此葉塊中還有沒有滿足條件的行。
如果建成了唯一索引,不需要判斷葉塊是否還有滿足條件的行,葉塊就隻需要讀一次,一共隻需要3次邏輯讀。
sql> drop index table1_id ;
index dropped.
sql> create unique index table1_id on
3
460
509
表空間和存儲結構這就說到這兒。本章中的例子,都是在oracle 11gr2中做的,在oracle 10g中做同樣例子時的注意事項也都随例子說明了。
另外,本章的測試都是以8kb塊大小為例的,其他塊大小下的情況,留給讀者親自動手測試。