分區(partitioning) 將一個表或索引物理地分解爲多個更小、更可管理的部分。就訪問數據庫的應用而言,邏輯上講只有一個表或一個索引,但在物理上這個表或索引可能由數十個物理分區組成。每一個分區都是一個獨立的對象,能夠獨自處理,也能夠做爲一個更大對象的一部分進行處理。數據庫
分區有利於管理很是大的表和索引,它使用了一種「分而治之」的邏輯。分區引入了分區鍵(partition key)的概念,分區鍵用於根據某個區間值(或範圍值)、特定值列表或散列函數值執行數據的彙集。分區好處以下:
(1) 提升數據的可用性
(2) 因爲從數據庫中去除了大段,相應地減輕了管理的負擔。在一個100GB的表上執行管理操做時(如重組來刪除移植的行,或者在「淨化」舊信息後回收表左邊的「空白」空間),與在各個10GB的表分區上執行10次一樣的操做相比,前者負擔要大得多。另外,經過使用分區,可讓淨化例程根本不留下空白空間,這就徹底消除了重組的必要!
(3) 改善某些查詢的性能:主要在大型倉庫環境中有這個好處,經過使用分區,能夠消除很大的數據區間,從而沒必要考慮它們,相應地根本不用訪問這些數據。但這在事務性系統中並不適用,由於這種系統自己就只是訪問少許的數據。
(4) 能夠把修改分佈到多個單獨的分區上,從而減小大容量OLTP系統上的競爭:若是一個段遭遇激烈的競爭,能夠把它分爲多個段,這就能夠獲得一個反作用:能成比例地減小競爭。併發
可用性的提升源自於每一個分區的獨立性。對象中一個分區的可用性(或不可用)並不意味着對象自己是不可用的。優化器知道有這種分區機制,會相應地從查詢計劃中去除未引用的分區。在一個大對象中若是一個分區不可用,查詢能夠消除這個分區而不予考慮,這樣Oracle就能成功地處理這個查詢。
爲了展現這種可用性的提升,咱們將創建一個散列分區表,其中有兩個分區,分別在單獨的表空間中。這裏將建立一個EMP表,它在EMPNO列上指定了一個分區鍵(EMPNO就是咱們的分區鍵)。在這種狀況下,這個結構意味着:對於插入到這個表中的每一行,會對EMPNO列的值計算散列,來肯定這一行將置於哪一個分區(及相應的表空間)中:app
scott@ORCL>create tablespace P2 datafile 'D:\app\Administrator\oradata\orcl\P2.dbf' 2 size 40m autoextend on maxsize 4G; 表空間已建立。 scott@ORCL>create tablespace P1 datafile 'D:\app\Administrator\oradata\orcl\P1.dbf' 2 size 40m autoextend on maxsize 4G; 表空間已建立。 scott@ORCL>CREATE TABLE emp 2 ( empno int, 3 ename varchar2(20) 4 ) 5 PARTITION BY HASH (empno) 6 ( partition part_1 tablespace p1, 7 partition part_2 tablespace p2 8 ) 9 / 表已建立。
接下來,咱們向表中插入一些數據,而後使用帶分區的擴展表名檢查各個分區的內容:less
scott@ORCL>insert into emp select empno, ename from emp_bak; 已建立14行。 scott@ORCL>select * from emp partition(part_1); EMPNO ENAME ---------- ---------------------------------------- 7782 CLARK 7839 KING 7934 MILLER 7369 SMITH 7876 ADAMS 7499 ALLEN 7654 MARTIN 7698 BLAKE 已選擇8行。 scott@ORCL>select * from emp partition(part_2); EMPNO ENAME ---------- ---------------------------------------- 7566 JONES 7788 SCOTT 7902 FORD 7521 WARD 7844 TURNER 7900 JAMES 已選擇6行。
經過使用散列分區,咱們讓Oracle隨機地(極可能均勻地)將數據分佈到多個分區上。咱們沒法控制數據要分佈到哪一個分區上;Oracle會根據生成的散列鍵值來肯定。ide
下面將其中一個表空間離線(例如,模擬一種磁盤出故障的狀況),使這個分區中的數據不可用:函數
scott@ORCL>alter tablespace p1 offline; 表空間已更改。
接下來,運行一個查詢,這個查詢將命中每個分區,能夠看到這個查詢失敗了:高併發
scott@ORCL>select * from emp; select * from emp * 第 1 行出現錯誤: ORA-00376: 此時沒法讀取文件 6 ORA-01110: 數據文件 6: 'D:\APP\ADMINISTRATOR\ORADATA\ORCL\P1.DBF'
不過,若是查詢不訪問離線的表空間,這個查詢就能正常工做;Oracle會消除離線分區而不予考慮。在這個特定的例子中,我使用了一個綁定變量,這只是爲了展現Oracle確定能消除離線分區:即便Oracle在查詢優化時不知道會訪問哪一個分區,也能在運行是不考慮離線分區:工具
scott@ORCL>variable n number scott@ORCL>exec :n := 7844; PL/SQL 過程已成功完成。 scott@ORCL>select * from emp where empno = :n; EMPNO ENAME ---------- ---------------------------------------- 7844 TURNER
總之,只要優化器能從查詢計劃消除分區,它就會這麼作。基於這一點,若是應用在查詢中使用了分區鍵,就能提升這些應用的可用性。
分區還能夠經過減小停機時間來提升可用性。例如,若是有一個100GB的表,它劃分爲50個2GB的分區,這樣就能更快地從錯誤中恢復。若是某個2GB的分區遭到破壞,如今恢復的時間就只是恢復一個2GB分區所需的時間,而不是恢復一個100GB表的時間。因此從兩個方面提升了可用性:
優化器可以消除分區,這意味着許多用戶可能甚至從未注意到某些數據是不可用的。
出現錯誤時的停機時間會減小,由於恢復所需的工做量大幅減小。oop
與在一個大對象上執行操做相比,在小對象上執行一樣的操做從本質上講更爲容易、速度更快,並且佔用的資源也更少。性能
例如,假設數據庫中有一個10GB的索引。若是須要重建這個索引,而該索引未分區,你就必須將整個10GB的索引做爲一個工做單元來重建。儘管能夠在線地重建索引,可是要徹底重建完整的10GB索引,仍是須要佔用大量的資源。至少須要在某處有10GB的空閒存儲空間來存放兩個索引的副本,還須要一個臨時事務日誌表來記錄重建索引期間對基表所作的修改。另外一方面,若是將索引自己劃分爲10個1GB的分區,就能夠一個接一個地單獨重建各個索引分區。如今只須要原先所需空閒空間的10%。另外,各個索引的重建也更快(多是原來的10倍),須要向新索引合併的事務修改也更少(到此爲止,在線索引重建期間發生的事務修改會更少)。
另外請考慮如下狀況:10GB索引的重建即將完成以前,若是出現系統或軟件故障會發生什麼。咱們所作的所有努力都會付諸東流。若是把問題分解,將索引劃分爲1GB的分區,你最多隻會丟掉重建工做的10%。
或者,你可能只須要重建所有彙集索引的10%,例如,只是「最新」的數據(活動數據)須要重組,而全部「較舊」的數據(至關靜態)不受影響。
最後,請考慮這樣一種狀況:你發現表中50%的行都是「移植」行,可能想進行修正。創建一個分區表將有利於這個操做。爲了「修正」移植行,你每每必須重建對象,在這種狀況下,就是要重建一個表。若是有一個100GB的表,就須要在一個很是大的「塊」(chunk)上連續地使用ALTER TABLE MOVE來執行這個操做。另外一方面,若是你有25個分區,每一個分區的大小爲4GB,就能夠一個接一個地重建各個分區。或者,若是你在空餘時間作這個工做,並且有充足的資源,甚至能夠在單獨的會話中並行地執行ALTER TABLE MOVE語句,這就極可能會減小整個操做所需的時間。對於一個未分區對象所能作的工做,分區對象中的單個分區幾乎都能作到。你甚至可能發現,移植行都集中在一個很小的分區子集中,所以,能夠只重建一兩個分區,而不是重建整個表。
這裏有一個小例子,展現瞭如何對一個有多個移植行的表進行重建。BIG_TABLE1和BIG_TABLE2都是從BIG_TABLE的一個10,000,000行的實例建立的。BIG_TABLE1是一個常規的未分區表,而BIG_TABLE2是一個散列分區表,有8個分區:
create tablespace big1 datafile 'D:\app\Administrator\oradata\orcl\big1.dbf' size 20m autoextend on maxsize 2G; 表空間已建立。 create table big_table1 ( ID, OWNER, OBJECT_NAME, SUBOBJECT_NAME, OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, CREATED, LAST_DDL_TIME, TIMESTAMP, STATUS, TEMPORARY, GENERATED, SECONDARY ) tablespace big1 as select ID, OWNER, OBJECT_NAME, SUBOBJECT_NAME, OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, CREATED, LAST_DDL_TIME, TIMESTAMP, STATUS, TEMPORARY, GENERATED, SECONDARY from big_table; 表已建立。 create tablespace big2 datafile 'D:\app\Administrator\oradata\orcl\big2.dbf' size 20m autoextend on maxsize 2G; 表空間已建立。 create table big_table2 ( ID, OWNER, OBJECT_NAME, SUBOBJECT_NAME, OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, CREATED, LAST_DDL_TIME, TIMESTAMP, STATUS, TEMPORARY, GENERATED, SECONDARY ) partition by hash(id) (partition part_1 tablespace big2, partition part_2 tablespace big2, partition part_3 tablespace big2, partition part_4 tablespace big2, partition part_5 tablespace big2, partition part_6 tablespace big2, partition part_7 tablespace big2, partition part_8 tablespace big2 ) as select ID, OWNER, OBJECT_NAME, SUBOBJECT_NAME, OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, CREATED, LAST_DDL_TIME, TIMESTAMP, STATUS, TEMPORARY, GENERATED, SECONDARY from big_table; 表已建立。
如今,每一個表都在本身的表空間中,因此咱們能夠很容易地查詢數據字典,來查看每一個表空間中已分配的空間和空閒空間:
select b.tablespace_name, mbytes_alloc, mbytes_free from ( select round(sum(bytes)/1024/1024) mbytes_free, tablespace_name from dba_free_space group by tablespace_name ) a, ( select round(sum(bytes)/1024/1024) mbytes_alloc, tablespace_name from dba_data_files group by tablespace_name ) b where a.tablespace_name (+) = b.tablespace_name and b.tablespace_name in ('BIG1','BIG2') / TABLESPACE_NAME MBYTES_ALLOC MBYTES _FREE ------------------------------------------------------------ ------------ ------ ----- BIG2 1362 17 BIG1 1345
BIG1和BIG2的大小都大約是1.3GB,每一個表空間都有344MB的空閒空間。咱們想建立第一個表BIG_TABLE1:
scott@ORCL>alter table big_table1 move; alter table big_table1 move * 第 1 行出現錯誤: ORA-01652: 沒法經過 1024 (在表空間 BIG1 中) 擴展 temp 段
但失敗了,BIG 1表空間中要有足夠的空閒空間來放下BIG_TABLE1的完整副本,同時它的原副本仍然保留,簡單地說,咱們須要一個很短的時間內有大約兩倍的存儲空間(可能多一點,也可能少移動,這取決於重建後表的大小)。如今試圖對BIG_TABLE2執行一樣的操做:
scott@ORCL>alter table big_table2 move; alter table big_table2 move * 第 1 行出現錯誤: ORA-14511: 不能對分區對象進行操做
這說明,Oracle在告訴咱們:沒法對這個「表」執行MOVE操做;咱們必須在表的各個分區上執行這個操做。能夠逐個地移動(相應地重建和重組)各個分區:
scott@ORCL>alter table big_table2 move partition part_1; 表已更改。 scott@ORCL>alter table big_table2 move partition part_2; 表已更改。 scott@ORCL>alter table big_table2 move partition part_3; 表已更改。 scott@ORCL>alter table big_table2 move partition part_4; 表已更改。 scott@ORCL>alter table big_table2 move partition part_5; 表已更改。 scott@ORCL>alter table big_table2 move partition part_6; 表已更改。 scott@ORCL>alter table big_table2 move partition part_7; 表已更改。 scott@ORCL>alter table big_table2 move partition part_8; 表已更改。
對於每一個移動,只須要有足夠的空閒空間來存放原來數據的1/8的副本!所以,假設有先前一樣多的空閒空間,這些命令就能成功。咱們須要的臨時資源將顯著減小。不只如此,若是在移動到PART_4後但在PART_5完成「移動」以前系統失敗了(例如,掉電),並不會丟失之前所作的全部工做,這與執行一個MOVE語句的狀況不一樣。前4個分區還是「移動」後的狀態,等系統恢復時,咱們能夠從分區PART_5繼續處理。
若是有數百個分區(或者更多),能夠編寫一個腳原本解決這個問題,前面的語句則變成如下腳本:
begin for x in ( select partition_name from user_tab_partitions where table_name = 'BIG_TABLE2' ) loop execute immediate 'alter table big_table2 move partition ' || x.partition_name; end loop; end; /
須要的全部信息都能在Oracle數據字典中找到,並且大多數實現了分區的站點都有一系列存儲過程,可用於簡化大量分區的管理。另外,許多GUI工具(如Enterprise Manager)也有一種內置的功能,能夠執行這種操做而無需你鍵入各條命令。
關於分區和管理,還有一個因素須要考慮,這就是在維護數據倉庫和歸檔中使用數據「滑動窗口」。在許多狀況下,須要保證數據在最後N個時間單位內一直在線。例如,假設須要保證最後12個月或最後5年的數據在線。若是沒有分區,這一般是一個大規模的INSERT,其後是一個大規模的DELETE。爲此有相對多的DML,而且會生成大量的redo和undo。若是進行了分區,則只需作下面的工做:
(1) 用新的月(或年,或者是其餘)數據加載一個單獨的表。
(2) 對這個表充分創建索引(這一步甚至能夠在另外一個實例中完成,而後傳送到這個數據庫中)。
(3) 將這個新加載(並創建了索引)的表附加到分區表的最後,這裏使用一個快速DDL命令:ALTER TABLE EXCHANGE PARTITION。
(4) 從分區表另外一端將最舊的分區去掉。
這樣一來,如今就能夠很容易地支持包含時間敏感信息的很是大的對象。舊數據很容易地從分區表中去除,若是再也不須要它,能夠簡單地將其刪除;或者也能夠歸檔到某個地方。新數據能夠加載到一個單獨的表中,這樣在加載、建索引等工做完成以前就不會影響分區表。
利用分區,原先讓人畏懼的操做(有時甚至是不可行的操做)會變得像在小數據庫中同樣容易。
分區最後一個總的(潛在)好處體如今改進語句(SELECT、INSERT、UPDATE、DELETE、MERGE)的性能方面。
看兩類語句,一種是修改信息的語句,另外一種是隻讀取信息的語句,並討論在這種狀況下能夠從分區獲得哪些好處。
修改數據庫中數據的語句有可能會執行並行DML(parallel DML,PDML)。採用PDML時,Oracle使用多個線程或進程來執行INSERT、UPDATE或DELETE, 而不是執行一個串行進程。在一個有充足I/O帶寬的多CPU主機上,對於大規模的DML操做,速度的提高可能至關顯著。在Oracle9i之前的版本中,PDML要求必須分區。若是表確實已經分區,Oracle會根據對象全部的物理分區數爲對象指定一個最大並行度。從很大程度上講,在Oracle9i及之後版本中這個限制已經放鬆,只有兩個突出的例外;若是但願在一個表上執行PDML,並且這個表的一個LOB列上有一個位圖索引,要並行執行操做就必須對這個表分區;另外並行度就限制爲分區數。不過,總的說來,使用PDML並不必定要求進行分區。
在只讀查詢(SELECT語句)的性能方面,分區對兩類特殊操做起做用:
分區消除(partition elimination):處理查詢時不考慮某些數據分區。
並行操做(parallel operation):並行全表掃描和並行索引區間掃描就是這種操做的例子。
由此獲得的好處不少程度上取決於使用何種類型的系統。
在OLTP系統中,不該該把分區看成一種大幅改善查詢性能的方法。實際上,在一個傳統的OLTP系統中,你必須很當心地應用分區,提防着不要對運行時性能產生負面做用。在傳統的OLTP系統中,大多數查詢極可能幾乎當即返回,並且大多數數據庫獲取可能都經過一個很小的索引區間掃描來完成。所以,以上所列分區性能方面可能的主要優勢在OLTP系統中表現不出來。分區消除只在大對象全面掃描時纔有用,由於經過分區消除,你能夠避免對對象的很大部分作全面掃描。不過,在一個OLTP環境中,原本就不是對大對象全面掃描。即便對索引進行了分區,就算是真的能在速度上有所提升,經過掃描較小索引所獲得的性能提高也是微乎其微的。若是某些查詢使用了一個索引,並且它們根本沒法消除任何分區,你可能會發現,完成分區以後查詢實際上運行得反而更慢 了,由於你如今要掃描五、10或20個更小的索引,而不是一個較大的索引。
儘管如此,有分區的OLTP系統確實也有可能獲得效率提示。例如,能夠用分區來減小競爭,從而提升併發度。能夠利用分區將一個表的修改分佈到多個物理分區上。並非只有一個表段和一個索引段,而是能夠有10個表分區和20個索引分區。這就像有20個表而不是1個表,相應地,修改期間就能減小對這個共享資源的競爭。
至於並行操做,你可能不但願在一個OLTP系統中執行並行查詢。你會慎用並行操做,而是交由DBA來完成重建、建立索引、收集統計信息等工做。事實上在一個OLTP系統中,查詢已經有如下特色:即索引訪問至關快,所以,分區不會讓索引訪問的速度有太大的提升(甚至根本沒有任何提升)。這並非說要絕對避免在OLTP系統中使用分區;而只是說不要期望經過分區來提供大幅的性能提高。儘管有效狀況下分區可以改善查詢的性能,可是這些狀況在大多數OLTP應用中並不成立。不過在OLTP系統中,你仍是能夠獲得另外兩個可能的好處:減輕管理負擔以及有更高的可用性。
在一個數據倉庫/決策支持系統中,分區不只是一個很強大的管理工具,還能夠加快處理的速度。例如,有一個大表,須要在其中執行一個即席查詢。老是按銷售定額(sales quarter)執行即席查詢,由於每一個銷售定額包含數十萬條記錄,而有數百萬條在線記錄。所以,你想查詢整個數據集中至關小的一部分,可是基於銷售定額來索引不太可行。這個索引會指向數十萬條記錄,以這種方式執行索引區間掃描會很糟糕。 處理許多查詢時都要求執行一個全表掃描,可是最後卻發現,一方面必須掃描數百萬條記錄,但另外一方面其中大多數記錄並不適用於咱們的查詢。若是使用一種明智的分區機制,就能夠按銷售定額來彙集數據,這樣在查詢某個給定銷售定額的數據時,就能夠只對這個銷售定額的數據進行全面掃描。這在全部可能的解決方案中是 最佳的選擇。
在一個數據倉庫/決策支持環境中,會頻繁地使用並行查詢。所以,諸如並行索引區間掃描或並行快速全面索引掃描等操做不只頗有意義,並且對咱們頗有好處。咱們但願充分地使用全部可用的資源,並行查詢就提供了這樣的一種途徑。所以,在數據倉庫環境中,分區就意味着頗有可能會加快處理速度。
目前Oracle中有4種對錶分區的方法:
區間分區:能夠指定應當存儲在一塊兒的數據區間。例如,時間戳在Jan-2005內的全部記錄都存儲在分區1中,時間戳在Feb-2005內的全部記錄都存儲在分區2中,依此類推。這多是Oracle中最經常使用的分區機制。
散列分區:這是指在一個列(或多個列)上應用一個散列函數,行會按這個散列值放在某個分區中。
列表分區:指定一個離散值集,來肯定應當存儲在一塊兒的數據。例如,能夠指定STATUS列值在(’A’,’M’,’Z’)中的行放在分區1中,STATUS值在(‘D’,’P’,’Q’)中的行放在分區2中,依此類推。
組合分區:這是區間分區和散列分區的一種組合,或者是區間分區與列表分區的組合。經過組合分區,你能夠先對某些數據應用區間分區,再在區間中根據散列或列表來選擇最後的分區。
區間分區表(range partitioned table)。下面的CREATE TABLE語句建立了一個使用RANGE_KEY_COLUMN列的區間分區表。RANGE_KEY_COLUMN值嚴格小於01-JAN-2005的全部數據要放在分區PART_1中,RANGE_KEY_COLUMN值嚴格小於01-JAN-2006的全部數據則放在分區PART_2中。不知足這兩個條件的全部數據(例如,RANGE_KEY_COLUMN值爲01-JAN-2007的行)將不能插入,由於它們沒法映射到任何分區:
CREATE TABLE range_example ( range_key_column date , data varchar2(20) ) PARTITION BY RANGE (range_key_column) ( PARTITION part_1 VALUES LESS THAN (to_date('01/01/2005','dd/mm/yyyy')), PARTITION part_2 VALUES LESS THAN (to_date('01/01/2006','dd/mm/yyyy')) ) / 表已建立。
爲了展現分區區間是嚴格小於某個值而不是小於或等於某個值,這裏插入的行是特別選擇的。咱們首先插入值15-DEC-2004,它確定要放在分區PART_1中。咱們還插入了日期/時間爲01-JAN-2005以前一秒(31-dec-2004 23:59:59)的行,這一行也會放到分區PART_1中,由於它小於01-JAN-2005。不過,插入的下一行日期/時間不是嚴格小於分區區間邊界。最後一行顯然應該放在分區PART_2中,由於它小於PART_2的分區區間邊界。
insert into range_example ( range_key_column, data ) values ( to_date('15/12/2004 00:00:00', 'dd/mm/yyyy hh24:mi:ss' ), 'application data...' ); 已建立 1 行。 insert into range_example ( range_key_column, data ) values ( to_date('01/01/2005 00:00:00', 'dd/mm/yyyy hh24:mi:ss' ), 'application data...' ); 已建立 1 行。 insert into range_example ( range_key_column, data ) values ( to_date('31/12/2004 23:59:59', 'dd/mm/yyyy hh24:mi:ss' ), 'application data...' ); 已建立 1 行。
能夠從各個分區分別執行SELECT語句,來確認確實如此:
select to_char(range_key_column,'dd-mon-yyyy hh24:mi:ss') from range_example partition (part_1); TO_CHAR(RANGE_KEY_COLUMN,'DD-MON-YYYYHH24:MI:SS') -------------------------------------------------- 15-12月-2004 00:00:00 31-12月-2004 23:59:59 select to_char(range_key_column,'dd-mon-yyyy hh24:mi:ss') from range_example partition (part_2); TO_CHAR(RANGE_KEY_COLUMN,'DD-MON-YYYYHH24:MI:SS') -------------------------------------------------- 01-1月 -2005 00:00:00
若是插入的日期超出上界會怎麼樣呢:
scott@ORCL>insert into range_example 2 ( range_key_column, data ) 3 values 4 ( to_date('15/12/2007 00:00:00', 5 'dd/mm/yyyy hh24:mi:ss' ), 6 'application data...' ); insert into range_example * 第 1 行出現錯誤: ORA-14400: 插入的分區關鍵字未映射到任何分區
假設你想像剛纔同樣,將2005年和2006年的日期分別彙集到各自的分區,可是另外你還但願將全部其餘日期都納入第三個分區。利用區間分區,這可使用MAXVALUE子句作到這一點,以下所示:
CREATE TABLE range_example ( range_key_column date , data varchar2(20) ) PARTITION BY RANGE (range_key_column) ( PARTITION part_1 VALUES LESS THAN (to_date('01/01/2005','dd/mm/yyyy')), PARTITION part_2 VALUES LESS THAN (to_date('01/01/2006','dd/mm/yyyy')) PARTITION part_3 VALUES LESS THAN (MAXVALUE) ) /
如今,向這個表插入一個行時,這一行確定會放入三個分區中的某一個分區中,而不會再拒絕任何行,由於分區PART_3能夠接受不能放在PART_1或PART_2中的任何RANG_KEY_COLUMN值(即便RANGE_KEY_COLUMN值爲null,也會插入到這個新分區中)。
對一個表執行散列分區(hash partitioning)時,Oracle會對分區鍵應用一個散列函數,以此肯定數據應當放在N個分區中的哪個分區中。Oracle建議N是2的一個冪(二、四、八、16等),從而獲得最佳的整體分佈。
散列分區設計爲能使數據很好地分佈在多個不一樣設備(磁盤)上,或者只是將數據彙集到更可管理的塊(chunk)上,爲表選擇的散列鍵應當是唯一的一個列或一組列,或者至少有足夠多的相異值,以便行能在多個分區上很好地(均勻地)分佈。
在這裏,咱們將建立一個有兩個分區的散列表。在此使用名爲HASH_KEY_COLUMN的列做爲分區鍵。Oracle會取這個列中的值,並計算它的散列值,從而肯定這一行將存儲在哪一個分區中:
CREATE TABLE hash_example ( hash_key_column date, data varchar2(20) ) PARTITION BY HASH (hash_key_column) ( partition part_1 tablespace p1, partition part_2 tablespace p2 ) / 表已建立。
若是使用散列分區,將無從控制一行最終會放在哪一個分區中。Oracle會應用散列函數,並根據散列的結果來肯定行會放在哪裏。行會按散列函數的「指示」放在某個分區中,也就是說,散列函數說這一行該放在哪一個分區,它就會放在哪一個分區中。若是改變散列分區的個數,數據會在全部分區中從新分佈(向一個散列分區表增長或刪除一個分區時,將致使全部數據都重寫,由於如今每一行可能屬於一個不一樣的分區)。
若是有一個大表,想對它「分而治之」,此時散列分區最有用。你不用管理一個大表,而只是管理8或16個 較小的「表」。從某種程度上講,散列分區對於提升可用性也頗有用;臨時丟掉一個散列分區,就能訪問全部餘下的分區。 也許有些用戶會受到影響,可是頗有可能不少用戶根本不受影響,可是頗有可能不少用戶根本不受影響。另外,恢復的單位如今也更小了。你不用恢復一個完整的大 表;而只需恢復表中的一小部分。最後一點,散列分區還有利於存在高度更新競爭的環境。咱們能夠不使一個段「很熱」,而是能夠將一個段散列分區爲16個「部分」,這樣一來,如今每一部分均可以接收修改。
分區數應該是2的冪。爲了便於說明,咱們創建了一個存儲過程,它會自動建立一個有N個分區的散列分區表(N是一個參數)。這個過程會構成一個動態查詢,按分區獲取其中的行數,再按分區顯示行數,並給出行數的一個簡單直方圖。最後,它會打開這個查詢,以便咱們看到結果。這個過程首先建立散列表。咱們將使用一個名爲T的表:
create or replace procedure hash_proc ( p_nhash in number, p_cursor out sys_refcursor ) authid current_user as l_text long; l_template long := 'select $POS$ oc, ''p$POS$'' pname, count(*) cnt ' || 'from t partition ( $PNAME$ ) union all '; begin begin execute immediate 'drop table t'; exception when others then null; end; execute immediate ' CREATE TABLE t ( id ) partition by hash(id) partitions ' || p_nhash || ' as select rownum from all_objects'; -- 接下來,動態構造一個查詢,按分區獲取行數。 -- 這裏使用了前面定義的「模板」查詢。 -- 對於每一個分區,咱們將使用分區擴展的表名來收集分區中的行數,並把全部行數合在一塊兒: for x in ( select partition_name pname, PARTITION_POSITION pos from user_tab_partitions where table_name = 'T' order by partition_position ) loop l_text := l_text || replace( replace(l_template, '$POS$', x.pos), '$PNAME$', x.pname ); end loop; -- 如今,取這個查詢,選出分區位置(PNAME)和該分區中的行數(CNT)。 -- 經過使用RPAD,能夠構造一個至關基本但頗有效的直方圖: open p_cursor for 'select pname, cnt, substr( rpad(''*'',30*round( cnt/max(cnt)over(),2),''*''),1,30) hg from (' || substr( l_text, 1, length(l_text)-11 ) || ') order by oc'; end; / 過程已建立。
若是針對輸入值4運行這個過程,這表示有4個散列分區,就會看到相似以下的輸出:
scott@ORCL>variable x refcursor scott@ORCL>set autoprint on scott@ORCL>exec hash_proc( 4, :x ); PL/SQL 過程已成功完成。 PNAM CNT ---- ---------- HG -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ---------------------------------------- p1 17914 ***************************** p2 17923 ***************************** p3 18122 ****************************** p4 17803 *****************************
這個簡單的直方圖展現了數據很均勻地分佈在這4個分區中。每一個分區中的行數都很接近。不過,若是將4改爲5,要求有5個散列分區,就會看到如下輸出:
scott@ORCL>exec hash_proc( 5, :x ); PL/SQL 過程已成功完成。 PNAM CNT ---- ---------- HG -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ---------------------------------------- p1 9016 *************** p2 17925 ***************************** p3 18125 ****************************** p4 17803 ***************************** p5 8898 **************
這個直方圖指出,第一個和最後一個分區中的行數只是另外三個分區中行數的一半。數據根本沒有獲得均勻的分佈。咱們會看到,若是有6個和7個散列分區,這種趨勢還會繼續:
scott@ORCL>exec hash_proc( 6, :x ); PL/SQL 過程已成功完成。 PNAM CNT ---- ---------- HG -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ---------------------------------------- p1 9018 *************** p2 9088 *************** p3 18128 ****************************** p4 17803 ***************************** p5 8898 ************** p6 8838 ************** 已選擇6行。 scott@ORCL>exec hash_proc( 7, :x ); PL/SQL 過程已成功完成。 PNAM CNT ---- ---------- HG -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ---------------------------------------- p1 9019 *************** p2 9089 *************** p3 9053 *************** p4 17805 ****************************** p5 8899 *************** p6 8838 *************** p7 9077 *************** 已選擇7行。
散列分區數再回到2的冪值(8)時,又能到達咱們的目標,實現均勻分佈:
scott@ORCL>exec hash_proc( 8, :x ); PL/SQL 過程已成功完成。 PNAM CNT ---- ---------- HG -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ---------------------------------------- p1 9019 ***************************** p2 9091 ****************************** p3 9055 ****************************** p4 8930 ***************************** p5 8900 ***************************** p6 8838 ***************************** p7 9079 ****************************** p8 8876 ***************************** 已選擇8行。
再繼續這個實驗,分區最多達到16個,你會看到若是分區數爲9~15,也存在一樣的問題,中間的分區存放的數據多,而兩頭的分區中數據少,數據的分佈是斜的;而達到16個分區時,你會再次看到數據分佈是直的。再達到32個分區和64個分區時也是如此。這個例子只是要指出:散列分區數要使用2的冪,這一點很是重要。
能夠根據離散的值列表來指定一行位於哪一個分區。若是能根據某個代碼來進行分區(如州代碼或區代碼),這一般頗有用。例如,你可能想把Maine州(ME)、New Hampshire州(NH)、Vermont州(VT)和Massachusetts州(MA)中全部人的記錄都歸至一個分區中,由於這些州相互之間捱得很近,並且你的應用按地理位置來查詢數據。相似地,你可能但願將Connecticut州(CT)、Rhode Island州(RI)和New York州(NY)的數據分組在一塊兒。
利用列表分區,咱們能夠很容易地完成這個定製分區機制:
create table list_example ( state_cd varchar2(2), data varchar2(20) ) partition by list(state_cd) ( partition part_1 values ( 'ME', 'NH', 'VT', 'MA' ), partition part_2 values ( 'CT', 'RI', 'NY' ) ) / 表已建立。
就像區間分區同樣,若是咱們想插入列表分區中未指定的一個值,Oracle會向客戶應用返回一個合適的錯誤。換句話說,沒有DEFAULT分區的列表分區表會隱含地施加一個約束(很是像表上的一個檢查約束):
scott@ORCL>insert into list_example values ( 'VA', 'data' ); insert into list_example values ( 'VA', 'data' ) * 第 1 行出現錯誤: ORA-14400: 插入的分區關鍵字未映射到任何分區
若是想把這個7個州分別彙集到各自的分區中,另外把其他的全部州代碼放在第三個分區中(或者,實際上對於所插入的任何其餘行,若是STATE_CD列值不是以上7個州代碼之一,就要放在第三個分區中),就可使用VALUES(DEFAULT)子句。在此,咱們將修改表,增長這個分區(也能夠在CREATE TABLE語句中使用這個子句):
alter table list_example add partition part_3 values ( DEFAULT ); 表已更改。 scott@ORCL>insert into list_example values ( 'VA', 'data' ); 已建立 1 行。
值列表中未顯式列出的全部值都會放到這個(DEFAULT)分區中。
一旦列表分區表有一個 DEFAULT 分區,就不能再向這個表中增長更多的分區了:
scott@ORCL>alter table list_example 2 add partition 3 part_4 values( 'CA', 'NM' ); alter table list_example * 第 1 行出現錯誤: ORA-14323: 在 DEFAULT 分區已存在時沒法添加分區
此時必須刪除DEFAULT分區,增長PART_4,再加回DEFAULT分區。緣由在於,原來DEFAULT分區能夠有列表分區鍵值爲CA或NM的行,但增長PART_4以後,這些行將再也不屬於DEFAULT分區。
組合分區是區間分區和散列分區的組合,或者是區間分區與列表分區的組合。
在組合分區中,頂層分區機制老是區間分區。第二級分區機制多是列表分區或散列分區。使用組合分區時,並無分區段,而只有子分區段。分區自己並無段(這就相似於分區表沒有段)。數據物理的存儲在子分區段上,分區成爲一個邏輯容器,或者是一個指向實際子分區的容器。
在下面的例子中,咱們將查看一個區間-散列組合分區,這兩層分區也可使用一樣的列集:
CREATE TABLE composite_example ( range_key_column date, hash_key_column int, data varchar2(20) ) PARTITION BY RANGE (range_key_column) subpartition by hash(hash_key_column) subpartitions 2 ( PARTITION part_1 VALUES LESS THAN(to_date('01/01/2005','dd/mm/yyyy')) (subpartition part_1_sub_1, subpartition part_1_sub_2 ), PARTITION part_2 VALUES LESS THAN(to_date('01/01/2006','dd/mm/yyyy')) (subpartition part_2_sub_1, subpartition part_2_sub_2 ) ) / 表已建立。
在區間-散列組合分區中,Oracle首先會應用區間分區規則,得出數據屬於哪一個區間。而後再應用散列函數,來肯定數據最後要放在哪一個物理分區中。
所以,利用組合分區,你就能把數據先按區間分解,若是認爲某個給定的區間還太大,或者認爲有必要作進一步的分區消除,能夠再利用散列或列表將其再作分解。每一個區間分區不須要有相同數目的子分區;
CREATE TABLE composite_range_list_example ( range_key_column date, code_key_column int, data varchar2(20) ) PARTITION BY RANGE (range_key_column) subpartition by list(code_key_column) ( PARTITION part_1 VALUES LESS THAN(to_date('01/01/2005','dd/mm/yyyy')) (subpartition part_1_sub_1 values( 1, 3, 5, 7 ), subpartition part_1_sub_2 values( 2, 4, 6, 8 ) ), PARTITION part_2 VALUES LESS THAN(to_date('01/01/2006','dd/mm/yyyy')) (subpartition part_2_sub_1 values ( 1, 3 ), subpartition part_2_sub_2 values ( 5, 7 ), subpartition part_2_sub_3 values ( 2, 4, 6, 8 ) ) ) / 表已建立。
在此,最後總共有5個分區:分區PART_1有兩個子分區,分區PART_2有3個子分區。
在前面所述的各類分區機制中,若是用於肯定分區的列有修改會發生什麼。須要考慮兩種狀況:
修改不會致使使用一個不一樣的分區;行仍屬於原來的分區。這在全部狀況下都獲得支持。
修改會致使行跨分區移動。只有當表啓用了行移動時才支持這種狀況;不然,會產生一個錯誤。
在前面的例子中,咱們向RANGE_EXAMPLE表的PART_1插入了兩行:
insert into range_example ( range_key_column, data ) values ( to_date( '15-12-2004 00:00:00', 'dd-MM-yyyy hh24:mi:ss' ), 'application data...' ); 已建立 1 行。 insert into range_example ( range_key_column, data ) values ( to_date( '01-01-2005 00:00:00', 'dd-MM-yyyy hh24:mi:ss' )-1/24/60/60, 'application data...' ); 已建立 1 行。 scott@ORCL>select * from range_example partition(part_1); RANGE_KEY_COLU DATA -------------- ---------------------------------------- 15-12月-04 application data... 31-12月-04 application data... 15-12月-04 application data... 31-12月-04 application data...
取其中一行,並更新其RANGE_KEY_COLUMN值,不過更新後它還能放在PART_1中:
update range_example set range_key_column = trunc(range_key_column) where range_key_column = to_date( '31-12-2004 23:59:59','dd-MM-yyyy hh24:mi:ss' ); 已更新2行。
不出所料,這會成功:行仍在分區PART_1中。接下來,再把RANGE_KEY_COLUMN更新爲另外一個值,但此次更新後的值將致使它屬於分區PART_2:
scott@ORCL>update range_example 2 set range_key_column = to_date('02-01-2005','dd-MM-yyyy') 3 where range_key_column = to_date('31-12-2004','dd-MM-yyyy'); update range_example * 第 1 行出現錯誤: ORA-14402: 更新分區關鍵字列將致使分區的更改
這會當即產生一個錯誤,由於咱們沒有顯式地啓用行移動。
能夠在這個表上啓用行移動(row movement),以容許從一個分區移動到另外一個分區。
這樣作有一個小小的反作用;行的ROWID會因爲更新而改變:
select rowid from range_example where range_key_column = to_date('31-12-2004','dd-MM-yyyy'); ROWID ------------------ AAAU6HAADAAAtjGAAB AAAU6HAADAAAtjGAAD alter table range_example enable row movement; 表已更改。 update range_example set range_key_column = to_date('02-01-2005','dd-MM-yyyy') where range_key_column = to_date('31-12-2004','dd-MM-yyyy'); 已更新2行。 select rowid from range_example where range_key_column = to_date('02-01-2005','dd-MM-yyyy'); ROWID ------------------ AAAU6IAADAAAtjWAAB AAAU6IAADAAAtjWAAC
既然知道執行這個更新時行的ROWID會改變,因此要啓用行移動,這樣才容許更新分區鍵。
執行行移動時,實際上在內部就好像先刪除了這一行,而後再將其從新插入。這會更新這個表上的索引,刪除舊的索引條目,再插入一個新條目。此時會完成DELETE再加一個INSERT的相應物理工做。不過,儘管在此執行了行的物理刪除和插入,在Oracle看來卻仍是一個更新,所以,不會致使INSERT和DELETE觸發器觸發,只有UPDATE觸發器會觸發。另外,因爲外鍵約束可能不容許DELETE的子表也不會觸發DELETE觸發器。不過,仍是要對將完成的額外工做有所準備;行移動的開銷比正常的UPDATE昂貴得多。
索引與表相似,也能夠分區。對索引進行分區有兩種可能的方法:
隨表對索引完成相應的分區:這也稱爲局部分區索引(locally pertitioned index)。每一個表分區都有一個索引分區,並且只索引該表分區。一個給定索引分區中的全部條目都指向一個表分區,表分區中的全部行都表示在一個索引分區中。
按區間對索引分區:這也稱爲全局分區索引(globally partitioned index)。在此,索引按區間分區,一個索引分區可能指向任何(和全部)表分區。
對於全局分區索引,索引分區數可能不一樣於表分區數。
因爲全局索引只按區間或散列分區,若是但願有一個列表或組合分區索引,就必須使用局部索引。局部索引會使用底層表相同的機制分區。
Oracle 劃分瞭如下兩類局部索引:
D 局部前綴索引(local prefixed index):在這些索引中,分區鍵在索引定義的前幾列上。例如, 一個表在名爲 LOAD_DATE 的列上進行區間分區,該表上的局部前綴索引就是 LOAD_DATE 做爲其索引列列表中的第一列。
D 局部非前綴索引(local nonprefixed index):這些索引不以分區鍵做爲其列列表的前幾列。 索引可能包含分區鍵列,也可能不包含。
這兩類索引均可以利用分區消除,它們都支持唯一性(只有非前綴索引包含分區鍵)等。事實上,使用局部前綴索引的查詢總容許索引分區消除,而使用局部非前綴索引的查詢可能不容許。
若是查詢首先訪問索引,它是否能消除分區徹底取決於查詢中的謂詞。
下面的代碼建立了一個表PARTITIONED_TABLE,它在一個數字列A上進行區間分區,使得小於2的值都在分區PART_1中,小於3的值則都在分區PART_2中:
CREATE TABLE partitioned_table ( a int, b int, data char(20) ) PARTITION BY RANGE (a) ( PARTITION part_1 VALUES LESS THAN(2) tablespace p1, PARTITION part_2 VALUES LESS THAN(3) tablespace p2 ) / 表已建立。
而後建立一個局部前綴索引 LOCAL_PREFIXED 和一個局部非前綴索引 LOCAL_NONPREFIXED :
scott@ORCL>create index local_prefixed on partitioned_table (a,b) local; 索引已建立。 scott@ORCL>create index local_nonprefixed on partitioned_table (b) local; 索引已建立。
接下來,向一個分區中插入一些數據,並收集統計信息:
insert into partitioned_table select mod(rownum-1,2)+1, rownum, 'x' from all_objects; 已建立71816行。 begin dbms_stats.gather_table_stats ( user, 'PARTITIONED_TABLE', cascade=>TRUE ); end; / PL/SQL 過程已成功完成。
將表空間P2離線,其中包含用於表和索引的PART_2分區:
scott@ORCL>alter tablespace p2 offline; 表空間已更改。
表空間P2離線後,Oracle就沒法訪問這些特定的索引分區。
如今查詢這個表,來看看不一樣的查詢須要哪些索引分區。第一個查詢編寫爲容許使用局部前綴索引:
scott@ORCL>select * from partitioned_table where a = 1 and b = 1; A B DATA ---------- ---------- ---------------------------------------- 1 1 x
這個查詢成功了,查看解釋計劃,將使用內置包DBMS_XPLAN來查看這個查詢訪問了哪些分區。輸出中的PSTART (分區開始)和PSTOP(分區結束)這兩列準確地顯示出,這個查詢要想成功須要哪些分區必須在線並且可用:
scott@ORCL>delete from plan_table; 已刪除0行。 scott@ORCL>explain plan for 2 select * from partitioned_table where a = 1 and b = 1; 已解釋。 scott@ORCL>select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ---------------------------------------- Plan hash value: 1622054381 -------------------------------------------------------------------------------- ---------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | -------------------------------------------------------------------------------- ---------------------------------------- | 0 | SELECT STATEMENT | | 1 | 29 | 2 (0)| 00:00:01 | | | | 1 | PARTITION RANGE SINGLE | | 1 | 29 | 2 (0)| 00:00:01 | 1 | 1 | | 2 | TABLE ACCESS BY LOCAL INDEX ROWID| PARTITIONED_TABLE | 1 | 29 | 2 (0)| 00:00:01 | 1 | 1 | |* 3 | INDEX RANGE SCAN | LOCAL_PREFIXED | 1 | | 1 (0)| 00:00:01 | 1 | 1 | -------------------------------------------------------------------------------- ---------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("A"=1 AND "B"=1) 已選擇15行。
所以,使用LOCAL_PREFIXED的查詢成功了。優化器能消除LOCAL_PREFIXED的PART_2不予考慮,由於在查詢中指定了A=1,並且在計劃中能夠清楚地看到PSTART和PSTOP都等於1。分區消除幫助了咱們。不過,第二個查詢卻失敗了:
scott@ORCL>select * from partitioned_table where b = 1; ERROR: ORA-00376: 此時沒法讀取文件 8 ORA-01110: 數據文件 8: 'D:\APP\ADMINISTRATOR\ORADATA\ORCL\P2.DBF'
經過使用一樣的技術,能夠看到這是爲何:
scott@ORCL>delete from plan_table; 已刪除4行。 scott@ORCL>explain plan for 2 select * from partitioned_table where b = 1; 已解釋。 scott@ORCL>select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ---------------------------------------- Plan hash value: 440752652 -------------------------------------------------------------------------------- ---------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | -------------------------------------------------------------------------------- ---------------------------------------- | 0 | SELECT STATEMENT | | 1 | 29 | 4 (0)| 00:00:01 | | | | 1 | PARTITION RANGE ALL | | 1 | 29 | 4 (0)| 00:00:01 | 1 | 2 | | 2 | TABLE ACCESS BY LOCAL INDEX ROWID| PARTITIONED_TABLE | 1 | 29 | 4 (0)| 00:00:01 | 1 | 2 | |* 3 | INDEX RANGE SCAN | LOCAL_NONPREFIXED | 1 | | 3 (0)| 00:00:01 | 1 | 2 | -------------------------------------------------------------------------------- ---------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("B"=1) 已選擇15行。
在此,優化器不能不考慮LOCAL_NONPREFIXED的PART_2,爲了查看是否有B=1,索引的PART_1和PART_2都必須檢查。在此,局部非前綴索引存在一個性能問題:它不能像前綴索引那樣,在謂詞中使用分區鍵;要使用非前綴索引,必須使用一個容許分區消除的查詢。
scott@ORCL>drop index local_prefixed; 索引已刪除。 scott@ORCL>select * from partitioned_table where a = 1 and b = 1; A B DATA ---------- ---------- ---------------------------------------- 1 1 x
它會成功,可是正如咱們所見,這裏使用了先前失敗的索引。該計劃顯示出,在此Oracle能利用分區消除,有了謂詞A=1,就有了足夠的信息可讓數據庫消除索引分區PART_2而不予考慮:
scott@ORCL>delete from plan_table; 已刪除4行。 scott@ORCL>explain plan for 2 select * from partitioned_table where a = 1 and b = 1; 已解釋。 scott@ORCL>select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ---------------------------------------- Plan hash value: 904532382 -------------------------------------------------------------------------------- ---------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | -------------------------------------------------------------------------------- ---------------------------------------- | 0 | SELECT STATEMENT | | 1 | 29 | 2 (0)| 00:00:01 | | | | 1 | PARTITION RANGE SINGLE | | 1 | 29 | 2 (0)| 00:00:01 | 1 | 1 | |* 2 | TABLE ACCESS BY LOCAL INDEX ROWID| PARTITIONED_TABLE | 1 | 29 | 2 (0)| 00:00:01 | 1 | 1 | |* 3 | INDEX RANGE SCAN | LOCAL_NONPREFIXED | 1 | | 1 (0)| 00:00:01 | 1 | 1 | -------------------------------------------------------------------------------- ---------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("A"=1) 3 - access("B"=1) 已選擇16行。
注意PSTART和PSTOP列值爲1和1,這就證實,優化器甚至對非前綴局部索引也能執行分區消除。
若是要頻繁地用如下查詢來查詢先前的表:
select ... from partitioned_table where a = :a and b = :b; select ... from partitioned_table where b = :b;
能夠考慮在(b,a)上使用一個局部非前綴索引。這個索引對於前面的兩個查詢都是有用的。(a,b)上的局部前綴索引只對第一個查詢有用。
要儘量保證查詢包含的謂詞容許索引分區消除。
爲了保證唯一性(這包括UNIQUE約束或PRIMARY KEY約束),若是你想使用一個局部索引來保證這個約束,那麼分區鍵必須包括在約束自己中。Oracle只保證索引分區內部的唯一性,而不能跨分區。Oracle會利用全局索引來保證唯一性。
在下面的例子中,咱們將建立一個區間分區表,它按一個名爲LOAD_TYPE的列分區,卻在ID列上有一個主鍵。爲此,能夠在一個沒有任何其餘對象的模式中執行如下CREATE TABLE語句,因此經過查看這個用戶所擁有的每個段,就能很容易地看出到底建立了哪些對象:
scott@ORCL> create user test identified by test; 用戶已建立。 scott@ORCL> grant connect, resource to test; 受權成功。 scott@ORCL>connect test/test 已鏈接。 CREATE TABLE partitioned ( load_date date, id int, constraint partitioned_pk primary key(id) ) PARTITION BY RANGE (load_date) ( PARTITION part_1 VALUES LESS THAN ( to_date('01/01/2000','dd/mm/yyyy') ) , PARTITION part_2 VALUES LESS THAN ( to_date('01/01/2001','dd/mm/yyyy') ) ) / 表已建立。 select segment_name, partition_name, segment_type from user_segments; SEGMENT_NAME -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -- PARTITION_NAME SEGMENT_TYPE ------------------------------------------------------------ ------------------- ----------------- PARTITIONED PART_1 TABLE PARTITION PARTITIONED PART_2 TABLE PARTITION PARTITIONED_PK INDEX
PARTITIONED_PK索引甚至沒有分區,它根本沒法進行局部分區。因爲認識到非唯一索引也能像唯一索引同樣保證主鍵,咱們想以此騙過Oracle,可是能夠看到這種方法也不能奏效:
CREATE TABLE partitioned ( timestamp date, id int ) PARTITION BY RANGE (timestamp) ( PARTITION part_1 VALUES LESS THAN ( to_date('01-01-2000','dd-MM-yyyy') ) , PARTITION part_2 VALUES LESS THAN ( to_date('01-01-2001','dd-MM-yyyy') ) ) / create index partitioned_idx on partitioned(id) local / select segment_name, partition_name, segment_type from user_segments; SEGMENT_NAME -------------------------------------------------------------------------------- PARTITION_NAME SEGMENT_TYPE ------------------------------------------------------------ ------------------- PARTITIONED PART_1 TABLE PARTITION PARTITIONED PART_2 TABLE PARTITION PARTITIONED_IDX PART_1 INDEX PARTITION PARTITIONED_IDX PART_2 INDEX PARTITION alter table partitioned add constraint partitioned_pk primary key(id) / alter table partitioned * 第 1 行出現錯誤: ORA-01408: 此列列表已索引
在此,Oracle試圖在ID上建立一個全局索引,卻發現辦不到,這是由於ID上已經存在一個索引。若是已建立的索引沒有分區,前面的語句就能工做,Oracle會使用這個索引來保證約束。
爲何局部分區索引不能保證唯一性(除非分區鍵是約束的一部分),緣由有兩方面。首先,若是Oracle容許如此,就會喪失分區的大多數好處。可用性和可擴縮性都會喪失殆盡,由於對於任何插入和更新,老是要求全部分區都必定可用,並且要掃描每個分區。你的分區越多,數據就會變得越不可用。另外,分區越多,要掃描的索引分區就越多,分區也會變得愈加不可擴縮。這樣作不只不能提供可用性和可擴縮性,相反,實際上反倒會削弱可用性和可擴縮性。
另外,假若局部分區索引能保證唯一性,Oracle就必須在事務級對這個表的插入和更新有效地串行化。這是由於,若是向PART_1增長ID=1,Oracle就必須以某種方式防止其餘人向PART_2增長ID=1。對此唯一的作法是防止別人修改索引分區PART_2,由於沒法經過對這個分區中的內容「鎖定」來作到(找不出什麼能夠鎖定)。
在一個OLTP系統中,唯一性約束必須由系統保證(也就是說,由Oracle保證),以確保數據的完整性。這意味着,應用的邏輯模型會對物理設計產生影響。唯一性約束能決定底層的表分區機制,影響分區鍵的選擇,或者指示你應該使用全局索引。
全局索引使用一種有別於底層表的機制進行分區。表能夠按一個TIMESTAMP列劃分爲10個分區,而這個表上的一個全局索引能夠按REGION列劃分爲5個分區。與局部索引不一樣,全局索引 只能是 前綴全局索引(prefixed global index)。不論用什麼屬性對索引分區,這些屬性都必須是索引鍵的前幾列。
全局分區索引能夠用於保證主鍵的唯一性,即便不包括表的分區鍵,也能夠有能保證唯一性的分區索引。
下面的例子建立了一個按TIMESTAMP分區的表,它有一個按ID分區的索引:
CREATE TABLE partitioned ( timestamp date, id int ) PARTITION BY RANGE (timestamp) ( PARTITION part_1 VALUES LESS THAN ( to_date('01-01-2000','dd-MM-yyyy') ) , PARTITION part_2 VALUES LESS THAN ( to_date('01-01-2001','dd-MM-yyyy') ) ) / 表已建立。 create index partitioned_index on partitioned(id) GLOBAL partition by range(id) ( partition part_1 values less than(1000), partition part_2 values less than (MAXVALUE) ) / 索引已建立。
MAXVALUE能夠不只能夠用於索引中,還能夠用於任何區間分區表中。全局索引有一個需求,即最高分區(最後一個分區)必須有一個值爲MAXVALUE的分區上界。這能夠確保底層表中的全部行都能放在這個索引中。
下面,在這個例子的最後,咱們將向表增長主鍵:
alter table partitioned add constraint partitioned_pk primary key(id) / 表已更改。
Oracle在使用咱們建立的索引來保證主鍵,能夠試着刪除這個索引來證實這一點:
test@ORCL>drop index partitioned_index; drop index partitioned_index * 第 1 行出現錯誤: ORA-02429: 沒法刪除用於強制惟一/主鍵的索引
爲了顯示Oracle不容許建立一個非前綴全局索引,只需執行下面的語句:
test@ORCL>create index partitioned_index2 2 on partitioned(timestamp,id) 3 GLOBAL 4 partition by range(id) 5 ( 6 partition part_1 values less than(1000), 7 partition part_2 values less than (MAXVALUE) 8 ) 9 / partition by range(id) * 第 4 行出現錯誤: ORA-14038: GLOBAL 分區索引必須加上前綴
全局索引必須是前綴索引。
要在何時使用全局索引呢?分析兩種不一樣類型的系統(數據倉庫和OLTP)。
原先數據倉庫和全局索引是至關互斥的。數據倉庫就意味着系統有某些性質,若有大量的數據出入。許多數據倉庫都實現了一種滑動窗口(sliding window)方法來管理數據,也就是說,刪除表中最舊的分區,併爲新加載的數據增長一個新分區。
滑動窗口和索引
在許多實現中,會隨着時間的推移向倉庫中增長數據,而最舊的數據會老化。在不少時候,這個數據會按一個日期屬性進行區間分區,因此最舊的數據多存儲在一個分區中,新加載的數據極可能都存儲在一個新分區中。每個月的加載過程涉及:
去除老數據:最舊的分區要麼被刪除,要麼與一個空表交換(將最舊的分區變爲一個表),從而容許對舊數據進行歸檔。
加載新數據並創建索引:將新數據加載到一個「工做」表中,創建索引並進行驗證。
關聯新數據:一旦加載並處理了新數據,數據所在的表會與分區表中的一個空分區交換,將表中的這些新加載的數據變成分區表中的一個分區(分區表會變得更大)。
這個過程會沒有重複,或者執行加載過程的任何週期重複;能夠是天天或每週。
在這個例子中,咱們將處理每一年的數據,並加載2004和2005財政年度的數據。這個表按TIMESTAMP列分區,並建立了兩個索引,一個是ID列上的局部分區索引,另外一個是TIMESTAMP列上的全局索引(這裏爲分區):
CREATE TABLE partitioned ( timestamp date, id int ) PARTITION BY RANGE (timestamp) ( PARTITION fy_2004 VALUES LESS THAN ( to_date('01-01-2005','dd-MM-yyyy') ) , PARTITION fy_2005 VALUES LESS THAN ( to_date('01-01-2006','dd-MM-yyyy') ) ) / insert into partitioned partition(fy_2004) select to_date('31-12-2004','dd-MM-yyyy')-mod(rownum,360), object_id from all_objects / insert into partitioned partition(fy_2005) select to_date('31-12-2005','dd-MM-yyyy')-mod(rownum,360), object_id from all_objects / create index partitioned_idx_local on partitioned(id) LOCAL / create index partitioned_idx_global on partitioned(timestamp) GLOBAL /
這就創建了咱們的「倉庫」表。數據按財政年度分區,並且最後兩年的數據在線。這個表有兩個索引:一個是LOCAL索引,另外一個是GLOBAL索引。咱們想作下面的工做:
(1) 刪除最舊的財政年度數據。咱們不想永遠地丟掉這個數據,而只是但願它老化,並將其歸檔。
(2) 增長最新的財政年度數據。加載、轉換、建索引等工做須要必定的時間。咱們想作這個工做,可是但願儘量不影響當前數據的可用性。
第一步是爲2004財政年度創建一個看上去就像分區表的空表。咱們將使用這個表與分區表中的FY_2004分區交換,將這個分區轉變成一個表,相應地是分區表中的分區爲空。這樣作的效果就是分區表中最舊的數據(實際上)會在交換以後被刪除:
test@ORCL>create table fy_2004 ( timestamp date, id int ); 表已建立。 test@ORCL>create index fy_2004_idx on fy_2004(id) ; 索引已建立。
對要加載的新數據作一樣的工做。咱們將建立並加載一個表,其結構就像是如今的分區表(可是它自己並非分區表):
test@ORCL>create table fy_2006 ( timestamp date, id int ); 表已建立。 insert into fy_2006 select to_date('31-12-2006','dd-MM-yyyy')-mod(rownum,360), object_id from all_objects / 已建立55717行。 test@ORCL>create index fy_2006_idx on fy_2006(id) nologging ; 索引已建立。
咱們將當前的滿分區變成一個空分區,並建立了一個包含FY_2004數據的「慢」表。並且,咱們完成了使用FY_2006數據的全部必要工做,這包括驗證數據、進行轉換以及準備這些數據所需完成的全部複雜任務。
如今可使用一個交換分區來更新「活動」數據:
alter table partitioned exchange partition fy_2004 with table fy_2004 including indexes without validation / alter table partitioned drop partition fy_2004 /
要把舊數據「老化」,所要作的僅此而已。咱們將分區變成一個滿表,而將空表變成一個分區。這是一個簡單的數據字典更新,瞬時就會完成,而不會發生大量的I/O。如今能夠將FY_2004表從數據庫中導出(可能要使用一個可移植的表空間)來實現歸檔。若是須要,還能夠很快地從新關聯這些數據。
接下來,咱們想「滑入」(即增長)新數據:
alter table partitioned add partition fy_2006 values less than ( to_date('01-01-2007','dd-MM-yyyy') ) / alter table partitioned exchange partition fy_2006 with table fy_2006 including indexes without validation /
一樣,這個工做也會當即完成;這是經過簡單的數據字典更新實現的。增長空分區幾乎不須要多少時間來處理。而後,將新建立的空分區與滿表交換(滿表與空分區交換),這個操做也會很快完成。新數據是在線的。
不過,經過查看索引,能夠看到下面的結果:
test@ORCL>select index_name, status from user_indexes; INDEX_NAME STATUS ------------------------------------------------------------ ---------------- FY_2006_IDX VALID FY_2004_IDX VALID PARTITIONED_IDX_LOCAL N/A PARTITIONED_IDX_GLOBAL UNUSABLE
當 然,在這個操做以後,全局索引是不可用的。因爲每一個索引分區可能指向任何表分區,而咱們剛纔取走了一個分區,並增長了一個分區,因此這個索引已經無效了。 其中有些條目指向咱們已經生成的分區,卻沒有任何條目指向剛增長的分區。使用了這個索引的任何查詢可能會失敗而沒法執行,或者若是咱們跳過不可用的索引, 儘管查詢能執行,但查詢的性能會受到負面影響(由於沒法使用這個索引):
test@ORCL>set autotrace on explain test@ORCL>select /*+ index( partitioned PARTITIONED_IDX_GLOBAL ) */ count(*) 2 from partitioned 3 where timestamp between sysdate-50 and sysdate ; select /*+ index( partitioned PARTITIONED_IDX_GLOBAL ) */ count(*) * 第 1 行出現錯誤: ORA-01502: 索引 'TEST.PARTITIONED_IDX_GLOBAL' 或這類索引的分區處於不可用狀態 select count(*) from partitioned where timestamp between sysdate-50 and sysdate;
所以,執行這個分區操做後,對於全局索引,咱們有如下選擇:
跳過索引,設置會話參數SKIP_UNUSABLE_INDEXES=TRUE來跳過索引。可是這樣一來,就丟失了索引所提供的性能提高。
讓查詢接收到一個錯誤,就像9i中同樣(SKIP_UNUSABLE_INDEX設置爲FALSE),在10g中,顯式地請求使用提示的任何查詢都會接收到錯誤。要想讓數據再次真正可用,必須重建這個索引。
到此爲止滑動窗口過程幾乎不會帶來任何停機時間,可是在咱們重建全局索引時,須要至關長的時間才能完成。若是查詢依賴於這些索引,在此期間它們的運行時查詢 性能就會受到負面影響,可能根本不會運行,也可能運行時得不到索引提供的好處。全部數據都必須掃描,並且要根據數據重建整個索引。若是表的大小爲數百DB,這會佔用至關多的資源。
「活動」全局索引維護
能夠在分區操做期間使用UPDATE GLOBAL INEXES子句來維護全局索引。這意味着,在你刪除一個分區、分解一個分區以及在分區上執行任何須要的操做時,Oracle會對全局索引執行必要的修改,保證它是最新的。因爲大多數分區操做都會致使全局索引無效,這個特徵對於須要提供數據連續訪問的系統來講是一個大福音。你會發現,經過犧牲分區操做的速度,能夠換取100%的數據可用性(儘管分區操做的整體響應時間會更慢)。簡單地說,若是數據倉庫不容許有停機時間,並且必須支持數據的滑入滑出等數據倉庫技術,這個特性就再合適不過了。
再來看前面的例子,若是分區操做在必要時使用了UPDATE GLOBAL INDEXES子句(在這個例子中,在ADD PARTITION語句上就沒有必要使用這個子句,由於新增長的分區中沒有任何行):
create table fy_2018 ( timestamp date, id int ); insert into fy_2018 select to_date('31-12-2018','dd-MM-yyyy')-mod(rownum,360), object_id from all_objects / create index fy_2018_idx on fy_2018(id) nologging ; alter table partitioned add partition fy_2018 values less than ( to_date('01-01-2018','dd-MM-yyyy')) / alter table partitioned exchange partition fy_2018 with table fy_2018 including indexes without validation UPDATE GLOBAL INDEXES / alter table partitioned drop partition fy_2018 UPDATE GLOBAL INDEXES /
就會發現索引徹底有效,不論在操做期間仍是操做以後這個索引都是可用的:
test@ORCL>select index_name, status from user_indexes; INDEX_NAME STATUS ------------------------------------------------------------ ---------------- FY_2018_IDX VALID FY_2006_IDX VALID FY_2004_IDX VALID PARTITIONED_IDX_LOCAL N/A PARTITIONED_IDX_GLOBAL UNUSABLE test@ORCL>set autotrace on explain test@ORCL>select count(*) 2 from partitioned 3 where timestamp between sysdate-50 and sysdate; COUNT(*) ---------- 0 執行計劃 ---------------------------------------------------------- Plan hash value: 2869581836 -------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | -------------------------------------------------------------------------------- -------------------------- | 0 | SELECT STATEMENT | | 1 | 9 | 84 (6)| 00:00:02 | | | | 1 | SORT AGGREGATE | | 1 | 9 | | | | | |* 2 | FILTER | | | | | | | | | 3 | PARTITION RANGE ITERATOR| | 3 | 27 | 84 (6)| 00:00:02 | KEY | KEY | |* 4 | TABLE ACCESS FULL | PARTITIONED | 3 | 27 | 84 (6)| 00:00:02 | KEY | KEY | -------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(SYSDATE@!-50<=SYSDATE@!) 4 - filter("TIMESTAMP"<=SYSDATE@! AND "TIMESTAMP">=SYSDATE@!-50) Note ----- - dynamic sampling used for this statement (level=2)
可是這裏要作一個權衡:咱們要在全局索引結構上執行INSERT和DELETE操做的相應邏輯操做。刪除一個分區時,必須刪除可能指向該分區的全部全局索引條目。執行表與分區的交換時,必須刪除指向原數據的全部全局索引條目,再插入指向剛滑入的數據的新條目。因此ALTER命令執行的工做量會大幅增長。
經過使用runstats,能測量出分區操做期間維護全局索引所執行的「額外」工做量。
咱們將滑出FY_2004,並滑入FY_2006,這就必須加入索引重建。因爲須要重建全局索引,所以滑動窗口實現將致使數據變得不可用。而後咱們再滑出FY_2005,並滑入FY_2007,不過這一次將使用UPDATE GLOBAL INDEXES子 句,來模擬提供徹底數據可用性的滑動窗口實現。這樣一來,即便在分區操做期間,數據也是可用的。採用這種方式,咱們就能測量出使用不一樣技術實現相同操做的性能,並對它們進行比較。咱們指望的結果是,第一種方法佔用的數據庫資源更少,所以會完成得「更快」,可是會帶來顯著的「停機時間」。第二種方法儘管會佔用更多的資源,並且總的來講可能須要花費更長的時間才能完成,可是不會帶來任何停機時間。
所以,若是用前面的例子,不過另外建立一個相似FY_2004的空FY_2005表,並建立一個相似FY_2006的滿FY_2007表,這樣就能夠測量索引重建方法之間有什麼差異,先來看「不太可用的方法」:
exec runStats_pkg.rs_start; CREATE TABLE partitioned ( timestamp date, id int ) PARTITION BY RANGE (timestamp) ( PARTITION fy_2004 VALUES LESS THAN ( to_date('01-01-2005','dd-MM-yyyy')) , PARTITION fy_2005 VALUES LESS THAN ( to_date('01-01-2006','dd-MM-yyyy')) ) / insert into partitioned partition(fy_2004) select to_date('31-12-2004','dd-MM-yyyy')-mod(rownum,360), object_id from all_objects / insert into partitioned partition(fy_2005) select to_date('31-12-2005','dd-MM-yyyy')-mod(rownum,360), object_id from all_objects / create index partitioned_idx_local on partitioned(id) LOCAL / create index partitioned_idx_global on partitioned(timestamp) GLOBAL / ********************************************************* create table fy_2004 ( timestamp date, id int ); create index fy_2004_idx on fy_2004(id) ; alter table partitioned exchange partition fy_2004 with table fy_2004 including indexes without validation / alter table partitioned drop partition fy_2004; ********************************************************* alter table partitioned add partition fy_2006 values less than ( to_date('01-01-2007','dd-MM-yyyy') ); create table fy_2006 ( timestamp date, id int ); insert into fy_2006 select to_date('31-12-2006','dd-MM-yyyy')-mod(rownum,360), object_id from all_objects / create index fy_2006_idx on fy_2006(id) nologging ; alter table partitioned exchange partition fy_2006 with table fy_2006 including indexes without validation; ********************************************************* alter index partitioned_idx_global rebuild; exec runStats_pkg.rs_middle;
下面是能夠提供高度可用性的UPDATE GLOBAL INDEXES方法:
create table fy_2005 ( timestamp date, id int ); create index fy_2005_idx on fy_2005(id) ; alter table partitioned exchange partition fy_2005 with table fy_2005 including indexes without validation update global indexes; alter table partitioned drop partition fy_2005 update global indexes; ******************************************************* alter table partitioned add partition fy_2007 values less than ( to_date('01-01-2008','dd-mm-yyyy') ); create table fy_2007 ( timestamp date, id int ); insert into fy_2007 select to_date('31-12-2007','dd-MM-yyyy')-mod(rownum,360), object_id from all_objects / create index fy_2007_idx on fy_2007(id) nologging ; alter table partitioned exchange partition fy_2007 with table fy_2007 including indexes without validation update global indexes; exec runStats_pkg.rs_stop;
能夠觀察到如下結果:
Run1 latches total versus runs -- difference and pct Run1 Run2 Diff Pct 1,437,419 540,149 -897,270 266.12%
索引重建方法確實運行得更快一些,從觀察到的耗用時間和CPU時間可見一斑。
查看這種方法生成的redo時,能夠看到UPDATE GLOBAL INDEXES生成的redo會多出許多,它是索引建立方法的230%,並且能夠想見,隨着爲表增長愈來愈多的全局索引,UPDATE GLOBAL INDEXES生成的redo數量還會進一步增長。UPDATE GLOBAL INDEXES生成的redo是不可避免的,不能經過NOLOGGING去掉,由於全局索引的維護不是其結構的徹底重建,而應該算是一種增量式「維護」。另外,因爲咱們維護着活動索引結構,必須爲之生成undo,萬一分區操做失敗,必須準備好將索引置回到它原來的樣子。並且要記住,undo受redo自己的保護,所以你看到的所生成的redo中,有些來自索引更新,有些來自回滾。若是增長另外一個(或兩個)全局索引,能夠很天然地想見這些數據量會增長。
UPDATE GLOBAL INDEXES是一種容許用資源耗費的增長來換取可用性的選項,能夠提供連續的可用性。
OLTP系統的特色是會頻繁出現許多小的讀寫事務,通常來說,在OLTP系統中,首要的是須要快速訪問所需的行,並且數據完整性很關鍵,另外可用性也很是重要。
在OLTP系統中,許多狀況下全局索引頗有意義。表數據能夠按一個鍵(一個列鍵)分區。不過,可能須要以多種不一樣的方式訪問數據。
這裏須要按多種不一樣的鍵來訪問應用中不一樣位置的EMPLOYEE數據,並且速度至上。
快速訪問
數據完整性
可用性
在一個OLTP系統中,能夠經過全局索引實現這些目標。
如下例子 顯示瞭如何用全局索引來達到以上所列的3個目標。這裏使用簡單的「單分區」全局索引,可是這與多個分區狀況下的全局索引也沒有不一樣(只有一點除外,增長索引分區時,可用性和可管理性會提升)。先建立一個表,它按位置LOC執行區間分區,根據咱們的規則,這會把全部小於‘C’的LOC值放在分區P1中,小於’D‘的LOC值則放在分區P2中,依此類推:
alter tablespace p1 online; alter tablespace p2 online; create tablespace P3 datafile 'D:\app\Administrator\oradata\orcl\P3.dbf' size 40m autoextend on maxsize 4G; create tablespace P4 datafile 'D:\app\Administrator\oradata\orcl\P4.dbf' size 40m autoextend on maxsize 4G; *********************************************************************** create table emp (EMPNO NUMBER(4) NOT NULL, ENAME VARCHAR2(10), JOB VARCHAR2(9), MGR NUMBER(4), HIREDATE DATE, SAL NUMBER(7,2), COMM NUMBER(7,2), DEPTNO NUMBER(2) NOT NULL, LOC VARCHAR2(13) NOT NULL ) partition by range(loc) ( partition p1 values less than('C') tablespace p1, partition p2 values less than('D') tablespace p2, partition p3 values less than('N') tablespace p3, partition p4 values less than('Z') tablespace p4 ) /
接下來修改這個表,在主鍵列上增長一個約束:
alter table emp add constraint emp_pk primary key(empno) /
EMPNO列上將有一個唯一索引,能夠支持和保證數據完整性。
在DEPTNO和JOB上建立另外兩個全局索引,以便經過這些屬性快速地訪問記錄:
create index emp_job_idx on emp(job) GLOBAL / create index emp_dept_idx on emp(deptno) GLOBAL / insert into emp select e.EMPNO,e.ENAME,e.JOB,e.MGR,e.HIREDATE,e.SAL,e.COMM,e.DEPTNO,d.LOC from scott.emp_bak e, scott.dept_bak d where e.deptno = d.deptno /
如今來看每一個分區中有什麼:
select 'p1' pname, empno, job, loc from emp partition(p1) union all select 'p2' pname, empno, job, loc from emp partition(p2) union all select 'p3' pname, empno, job, loc from emp partition(p3) union all select 'p4' pname, empno, job, loc from emp partition(p4) / PNAM EMPNO JOB LOC ---- ---------- ------------------ -------------------------- p2 7499 SALESMAN CHICAGO 7521 SALESMAN CHICAGO 7654 SALESMAN CHICAGO 7698 MANAGER CHICAGO 7844 SALESMAN CHICAGO 7900 CLERK CHICAGO p3 7369 CLERK DALLAS 7566 MANAGER DALLAS 7788 ANALYST DALLAS 7876 CLERK DALLAS 7902 ANALYST DALLAS p4 7782 MANAGER NEW YORK 7839 PRESIDENT NEW YORK 7934 CLERK NEW YORK 已選擇14行。
這顯示了數據按位置在各個分區中的分佈。如今能夠檢查一些查詢計劃,來查看會有怎樣的性能:
variable x varchar2(30); begin dbms_stats.set_table_stats ( user, 'EMP', numrows=>100000, numblks => 10000 ); end; / ******************************************************* delete from plan_table; explain plan for select empno, job, loc from emp where empno = :x; ******************************************************* abc@ORCL>select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------- ---------------------------------------- Plan hash value: 3656192650 -------------------------------------------------------------------------------- ----------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU )| Time | Pstart| Pstop | -------------------------------------------------------------------------------- ----------------------------- | 0 | SELECT STATEMENT | | 1 | 27 | 0 (0 )| 00:00:01 | | | | 1 | TABLE ACCESS BY GLOBAL INDEX ROWID| EMP | 1 | 27 | 0 (0 )| 00:00:01 | ROWID | ROWID | |* 2 | INDEX UNIQUE SCAN | EMP_PK | 1 | | 0 (0 )| 00:00:01 | | | -------------------------------------------------------------------------------- ----------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("EMPNO"=TO_NUMBER(:X)) 已選擇14行。
這裏的計劃顯示出對未分區索引EMP_PK(爲支持主鍵所建立)有一個INDEX UNIQUE SCAN。而後還有一個TABLE ACCESS GLOBAL INDEX ROWID,其PSTART和PSTOP爲ROWID/ROWID,這說明從索引獲得ROWID時,它會準確地告訴咱們讀哪一個索引分區來獲得這一行。這個索引訪問與未分區表上的訪問一樣有效,並且爲此會執行一樣數量的I/O。這只是一個簡單的單索引唯一掃描,其後是「根據ROWID來獲得這一行」。如今,咱們來看一個全局索引,即JOB上的全局索引:
abc@ORCL>delete from plan_table; 已刪除3行。 abc@ORCL>explain plan for 2 select empno, job, loc from emp where job = :x; 已解釋。 abc@ORCL>select * from table(dbms_xplan.display); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ---------------------------------------- Plan hash value: 475001586 -------------------------------------------------------------------------------- ---------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | -------------------------------------------------------------------------------- ---------------------------------- | 0 | SELECT STATEMENT | | 1000 | 27000 | 1 (0)| 00:00:01 | | | | 1 | TABLE ACCESS BY GLOBAL INDEX ROWID| EMP | 1000 | 27000 | 1 (0)| 00:00:01 | ROWID | ROWID | |* 2 | INDEX RANGE SCAN | EMP_JOB_IDX | 1 | | 1 (0)| 00:00:01 | | | -------------------------------------------------------------------------------- ---------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("JOB"=:X) 已選擇14行。
固然,對於INDEX RANGE SCAN,能夠看到相似的結果。在此使用了咱們的索引,並且能夠對底層數據提供高速的OLTP訪問。若是索引進行了分區,則必須是前綴索引,並保證索引分區消除;所以,這些索引也是可擴縮的,這說明咱們能夠對其分區,並且能觀察到相似的行爲。
最後,下面來看可用性方面,全局分區索引與局部分區索引有着一樣的高度可用性。考慮如下例子:
abc@ORCL>alter tablespace p1 offline; 表空間已更改。 abc@ORCL>alter tablespace p2 offline; 表空間已更改。 abc@ORCL>alter tablespace p3 offline; 表空間已更改。 abc@ORCL>select empno, job, loc from emp where empno = 7782; EMPNO JOB LOC ---------- ------------------ -------------------------- 7782 MANAGER NEW YORK
即便表中大多數底層數據都不可用,仍是能夠經過索引訪問任何可用的數據。只要咱們想要的EMPNO在可用的表空間中,並且GLOBAL索引可用,就能夠利用GLOBAL索引來訪問數據。
不過,在這種狀況下,其餘類型的查詢不會(並且不能)工做:
abc@ORCL>select empno, job, loc from emp where job = 'CLERK'; select empno, job, loc from emp where job = 'CLERK' * 第 1 行出現錯誤: ORA-00376: 此時沒法讀取文件 8 ORA-01110: 數據文件 8: 'D:\APP\ADMINISTRATOR\ORADATA\ORCL\P2.DBF'
全部分區中都有CLERK數據,因爲3個表空間離線,這一點確實會對咱們帶來影響。
若是能夠由索引來回答查詢,就要避免TABLE ACCESS BY ROWID,數據不可用的事實並不重要:
abc@ORCL>select count(*) from emp where job = 'CLERK'; COUNT(*) ---------- 4
在這種狀況下,因爲Oracle並不須要表,大多數分區離線的事實也不會影響這個查詢。因爲OLTP系統中這種優化(即只使用索引來回答查詢)很常見,因此不少應用都不會由於數據離線而受到影響。如今所要作的只是儘快地讓離線數據可用(將其恢復)。
在一個數據倉庫中,若是查詢頻繁地全面掃描很大的數據表,經過消除大段的數據,分區可以對這些查詢有很好的影響。假設你有一個100萬行的表,其中有一個時間戳屬性。你的查詢要從這個表中獲取一年的數據(其中有10年的數據)。查詢使用了一個全表掃描來獲取這個數據。若是按時間戳分區,例如每月一個分區,就能夠只對1/10的數據進行全面掃描(假設各年的數據是均勻分佈的)。經過分區消除,90%的數據均可以不考慮,查詢每每會運行得更快。
如今,再來看若是OLTP系統中有一個相似的表。在這種應用中,你確定不會獲取100萬行表中10%的數據,所以,儘管數據倉庫中能夠獲得大幅的速度提高,但這種提高在事務性系統中得不到。所以,通常來講,在OLTP系統中達不到第一種狀況(不會是查詢更快),你不會主要由於提供性能而應用分區。就算是要應用分區,也每每是爲了提供可用性以及獲得管理上的易用性。在一個OLTP系統中,即便是要確保達到第二點(也就是說,對查詢的性能沒有影響,而不管是負面影響仍是正面影響),也並不是垂手可得,而須要付出努力。不少時候,你的目標可能只是應用分區而不影響查詢響應時間。
例若有1000萬行,決定將數據分區。可是經過查看數據,卻發現沒有哪一個屬性能夠用於區間分區(RANGE partitioning)。根本沒有合適的屬性來執行區間分區。一樣,列表分區(LIST partitioning)也不可行。因此,想對主鍵執行散列分區,而主鍵剛好填充爲一個Oracle序號,主鍵是唯一的,並且易於散列,另外不少查詢都有如下形式:SELECT * FROM T WHERE PRIMARY_KEY = :X。
可是,對這個對象還有另一些並不是這種形式的查詢,假設當前表其實是ALL_OBJECTS字典視圖,儘管在內部許多查詢的形式都是WHERE OBJECT_ID = :X,但最終用戶還會頻繁地對應用發出如下請求:
顯示SCOTT中EMP表的詳細信息(WHERE OWNER=:0 AND OBJECT_TYPE=:T AND OBJECT_NAME=:N)。
顯示SCOTT所擁有的全部表(WHERE OWNER=:0 AND OBJECT_TYPE=:T)。
顯示SCOTT所擁有的全部對象(WHERE OWNER=:0).
爲了支持這些查詢,在(OWNER.OBJECT_TYPE.OBJECT_NAME)上有一個局部索引。
最後將表重建以下,它有16個散列分區:
create table t partition by hash(object_id) partitions 16 as select * from all_objects; create index t_idx on t(owner,object_type,object_name) LOCAL / begin dbms_stats.gather_table_stats ( user, 'T', cascade=>true); end; /
接下來執行經典的OLTP查詢(你知道這些查詢會頻繁地運行):
variable o varchar2(30) variable t varchar2(30) variable n varchar2(30) exec :o := 'SCOTT'; :t := 'TABLE'; :n := 'EMP'; select * from t where owner = :o and object_type = :t and object_name = :n / select * from t where owner = :o and object_type = :t / select * from t where owner = :o /
可是能夠注意到,運行以上代碼時,查看所獲得的TKPROF報告會有如下性能特徵:
select * from t where owner = :o and object_type = :t and object_name = :n call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 3 0.00 0.02 0 32 0 0 Rows Row Source Operation ------- --------------------------------------------------- 0 PARTITION HASH ALL PARTITION: 1 16 (cr=32 pr=0 pw=0 time=0 us cost=18 size=97 card=1) 0 TABLE ACCESS BY LOCAL INDEX ROWID T PARTITION: 1 16 (cr=32 pr=0 pw=0 time=0 us cost=18 size=97 card=1) 0 INDEX RANGE SCAN T_IDX PARTITION: 1 16 (cr=32 pr=0 pw=0 time=0 us cost=17 size=0 card=1)(object id 85931) ******************************************************************************** select * from t where owner = :o and object_type = :t call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 6 0.00 0.00 0 54 0 39 Rows Row Source Operation ------- --------------------------------------------------- 39 PARTITION HASH ALL PARTITION: 1 16 (cr=54 pr=0 pw=0 time=0 us cost=59 size=5820 card=60) 39 TABLE ACCESS BY LOCAL INDEX ROWID T PARTITION: 1 16 (cr=54 pr=0 pw=0 time=34 us cost=59 size=5820 card=60) 39 INDEX RANGE SCAN T_IDX PARTITION: 1 16 (cr=35 pr=0 pw=0 time=4 us cost=17 size=0 card=60)(object id 85931) ******************************************************************************** select * from t where owner = :o call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 15 0.00 0.00 0 1091 0 175 Rows Row Source Operation ------- --------------------------------------------------- 175 PARTITION HASH ALL PARTITION: 1 16 (cr=1091 pr=0 pw=0 time=0 us cost=313 size=232315 card=2395) 175 TABLE ACCESS FULL T PARTITION: 1 16 (cr=1091 pr=0 pw=0 time=69 us cost=313 size=232315 card=2395)
這個查詢必須查看每個索引分區,由於對應SCOTT的條目能夠在每個索引分區中。索引按OBJECT_ID執行邏輯散列分區,因此若是查詢使用了這個索引,但在謂詞中沒有引用OBJECT_ID,全部這樣的查詢都必須考慮每個索引分區!
與未實現分區的同一個表相比較,會發現如下結果:
select * from t where owner = :o and object_type = :t and object_name = :n call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 3 0.00 0.02 0 1033 0 0 Rows Row Source Operation ------- --------------------------------------------------- 0 TABLE ACCESS FULL T (cr=1033 pr=0 pw=0 time=0 us cost=287 size=97 card=1) ******************************************************************************** select * from t where owner = :o and object_type = :t call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 6 0.01 0.00 0 1036 0 38 Rows Row Source Operation ------- --------------------------------------------------- 38 TABLE ACCESS FULL T (cr=1036 pr=0 pw=0 time=0 us cost=287 size=5820 card=60) ******************************************************************************** select * from t where owner = :o call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 16 0.00 0.00 0 1046 0 189 Rows Row Source Operation ------- --------------------------------------------------- 189 TABLE ACCESS FULL T (cr=1046 pr=0 pw=0 time=0 us cost=287 size=232412 card=2396)
是對索引執行全局分區。例如,繼續看這個T_IDX例子,能夠選擇對索引進行散列分區,Oracle會取OWNER值,將其散列到1~16之間的一個分區,並把索引條目放在其中。如今,再次查看這3個查詢的TKPROF信息【表未分區--索引分區】:
create index t_idx on t(owner,object_type,object_name) global partition by hash(owner) partitions 16 /
select * from t where owner = :o and object_type = :t and object_name = :n call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 3 0.01 0.03 1 2 0 0 Rows Row Source Operation ------- --------------------------------------------------- 0 PARTITION HASH SINGLE PARTITION: KEY KEY (cr=2 pr=1 pw=0 time=0 us cost=2 size=97 card=1) 0 TABLE ACCESS BY INDEX ROWID T (cr=2 pr=1 pw=0 time=0 us cost=2 size=97 card=1) 0 INDEX RANGE SCAN T_IDX PARTITION: KEY KEY (cr=2 pr=1 pw=0 time=0 us cost=1 size=0 card=1)(object id 85949) ******************************************************************************** select * from t where owner = :o and object_type = :t call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 6 0.00 0.00 0 29 0 38 Rows Row Source Operation ------- --------------------------------------------------- 38 PARTITION HASH SINGLE PARTITION: KEY KEY (cr=29 pr=0 pw=0 time=0 us cost=45 size=5820 card=60) 38 TABLE ACCESS BY INDEX ROWID T (cr=29 pr=0 pw=0 time=0 us cost=45 size=5820 card=60) 38 INDEX RANGE SCAN T_IDX PARTITION: KEY KEY (cr=5 pr=0 pw=0 time=185 us cost=2 size=0 card=60)(object id 85949) ******************************************************************************** select * from t where owner = :o call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 16 0.01 0.00 0 1046 0 189 Rows Row Source Operation ------- --------------------------------------------------- 189 TABLE ACCESS FULL T (cr=1046 pr=0 pw=0 time=0 us cost=287 size=232412 card=2396)
【表分區--索引分區】
create table t partition by hash(object_id) partitions 16 as select * from all_objects; create index t_idx on t(owner,object_type,object_name) global partition by hash(owner) partitions 16 /
select * from t where owner = :o and object_type = :t and object_name = :n call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 3 0.00 0.00 0 2 0 0 Rows Row Source Operation ------- --------------------------------------------------- 0 PARTITION HASH SINGLE PARTITION: KEY KEY (cr=2 pr=0 pw=0 time=0 us cost=1 size=158 card=1) 0 TABLE ACCESS BY GLOBAL INDEX ROWID T PARTITION: ROW LOCATION ROW LOCATION (cr=2 pr=0 pw=0 time=0 us cost=1 size=158 card=1) 0 INDEX RANGE SCAN T_IDX PARTITION: KEY KEY (cr=2 pr=0 pw=0 time=0 us cost=1 size=0 card=1)(object id 85983) ******************************************************************************** select * from t where owner = :o and object_type = :t call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 6 0.00 0.00 0 41 0 38 Rows Row Source Operation ------- --------------------------------------------------- 38 PARTITION HASH SINGLE PARTITION: KEY KEY (cr=41 pr=0 pw=0 time=0 us cost=33 size=6004 card=38) 38 TABLE ACCESS BY GLOBAL INDEX ROWID T PARTITION: ROW LOCATION ROW LOCATION (cr=41 pr=0 pw=0 time=0 us cost=33 size=6004 card=38) 38 INDEX RANGE SCAN T_IDX PARTITION: KEY KEY (cr=5 pr=0 pw=0 time=0 us cost=2 size=0 card=38)(object id 85983) ******************************************************************************** select * from t where owner = :o call count cpu elapsed disk query current rows ------- ------ -------- ---------- ---------- ---------- ---------- ---------- total 18 0.00 0.00 0 204 0 221 Rows Row Source Operation ------- --------------------------------------------------- 221 PARTITION HASH SINGLE PARTITION: KEY KEY (cr=204 pr=0 pw=0 time=660 us cost=178 size=34918 card=221) 221 TABLE ACCESS BY GLOBAL INDEX ROWID T PARTITION: ROW LOCATION ROW LOCATION (cr=204 pr=0 pw=0 time=440 us cost=178 size=34918 card=221) 221 INDEX RANGE SCAN T_IDX PARTITION: KEY KEY (cr=18 pr=0 pw=0 time=0 us cost=3 size=0 card=221)(object id 85983)
散列分區索引沒法執行區間掃描。通常來講,它最適於徹底相等性比較(是否相等或是否在列表中)。若是想使用前面的索引來查詢WHERE OWNER > :X,就沒法使用分區消除來執行一個簡單的區間掃描,你必須退回去檢查所有的16個散列分區。
通常來說,對於OLTP中的數據獲取,分區確實沒有正面的影響。可是對於高度併發環境中的數據修改,分區則可能提供顯著的好處。
考慮一個至關簡單的例子,有一個表,並且只有一個索引,在這個表中再增長一個主鍵。若是沒有分區,實際上這裏只有一個表:全部插入都會插入到這個表中。對這個表的freelist可能存在競爭。另外,OBJECT_ID列上的主鍵索引是一個至關「重」的右側索引。假設主鍵列由一個序列來填充;所以,全部插入都會放到最右邊的塊中,這就會致使緩衝區等待。另外只有一個要競爭的索引結構T_IDX。目前看來,「單個」的項目太多了(只有一個表,一個索引等)。
再來看分區的狀況。按OBJECT_ID將表散列分區爲16個分區。如今就會競爭16個「表」,並且只會有1/16個「右側」,每一個索引結構只會接收之前1/16的工做負載,等等。也就是說,在一個高度併發環境中可使用分區來減小競爭。與沒有分區相比,數據的分區處理自己會佔用更多的CPU時間。也就是說,若是沒有分區,數只有一個去處,但有了分區後,則須要用更多的CPU時間來查明要把數據放在哪裏。
使用索引來獲取數據時,並不會自動地獲取有序的數據。要以某種有序順序來獲取數據,唯一的辦法就是在查詢上使用ORDER BY。若是查詢不包含ORDER BY語句,就不能對數據的有序順序作任何假設。
能夠用一個小例子來講明。建立了一個小表(ALL_USERS的一個副本),並建立一個散列分區索引,在USER_ID列上有4個分區:
create table t as select * from all_users / create index t_idx on t(user_id) global partition by hash(user_id) partitions 4 /
如今,咱們要查詢這個表,要求Oracle使用這個索引。注意數據的順序:
scott@ORCL>set autotrace on explain scott@ORCL>select /*+ index( t t_idx ) */ user_id 2 from t 3 where user_id > 0 4 / USER_ID ---------- 30 46 53 54 55 61 74 76 79 88 129 2147483638 9 31 32 42 43 67 70 83 86 90 94 5 21 56 65 72 78 85 89 112 14 45 57 75 84 87 已選擇38行。 執行計劃 ---------------------------------------------------------- Plan hash value: 3357014883 -------------------------------------------------------------------------------- ------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pst art| Pstop | -------------------------------------------------------------------------------- ------------ | 0 | SELECT STATEMENT | | 38 | 494 | 4 (0)| 00:00:01 | | | | 1 | PARTITION HASH ALL| | 38 | 494 | 4 (0)| 00:00:01 | 1 | 4 | |* 2 | INDEX RANGE SCAN | T_IDX | 38 | 494 | 4 (0)| 00:00:01 | 1 | 4 | -------------------------------------------------------------------------------- ------------ Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("USER_ID">0) Note ----- - dynamic sampling used for this statement (level=2) scott@ORCL>set autotrace off
因此,即便Oracle在區間掃描中使用了索引,數據顯然也不是有序的。實際上,能夠觀察到這個數據存在一個模式。這裏有「4個有序」的結果。咱們觀察到的結果是:Oracle會從4個散列分區一個接一個地返回「有序的數據」。
在此只是一個警告:除非你的查詢中有一個ORDER BY,不然不要期望返回的數據會按某種順序排序。(另外,GROUP BY也不會執行排序!ORDER BY 是無可替代的)。
在數據庫中可能只是插入審計跟蹤信息,而這部分數據在正常操做期間從不獲取。審計跟蹤信息主要做爲一種證據,這是一種過後證據。這些證據是必要的,可是從很 多方面來說,這些數據只是放在磁盤上,佔用着空間,並且所佔的空間至關大。而後必須每月或每一年(或者每隔一段時間)對其淨化或歸檔。若是審計從一開始就 設計不當,最後極可能置你於「死地」。從如今算起,若是7年後須要第一次對舊數據進行淨化或歸檔時你纔開始考慮如何來完成這一工做,那就太遲了。除非你作了適當的設計,不然取出舊信息實在是件痛苦的事情。
下面來看兩種技術:分區和段空間壓縮。 利用這些技術,審計不只是能夠忍受的,並且很容易管理,而且將佔用更少的空間。第二個技術可能不那麼明顯,由於段空間壓縮只適用於諸如直接路徑加載之類的 大批量操做,而審計跟蹤一般一次只插入一行,也就是事件發生時才插入。這裏的技巧是要將滑動窗口分區與段空間壓縮結合起來。
假設咱們決定按月對審計跟蹤信息分區。在第一個業務月中,咱們只是向分區表中插入信息;這些插入使用的是「傳統路徑」,而不是直接路徑,所以沒有壓縮。在這 個月結束以前,如今咱們要向表中增長一個新的分區,以容納下個月的審計活動。下個月開始後不久,咱們會對上個月的審計跟蹤信息執行一個大批量操做,具體來 講,咱們將使用ALTER TABLE命令來移動上個月的分區,這還有壓縮數據的做用。實際上,如 果再進一步,能夠將這個分區從一個可讀寫表空間(如今它必然在一個可讀寫表空間中)移動到一個一般只讀的表空間中(其中包含對應這個審計跟蹤信息的其餘分 區)。採用這種方式,就能夠一個月備份一次表空間(將分區移動到這個表空間以後才備份);這就能確保有一個正確、乾淨的當前表空間只讀副本;而後在這個月 再也不對其備份。審計跟蹤信息能夠有如下表空間:
一個當前在線的讀寫表空間,它會像系統中每個其餘的正常表空間同樣獲得備份。這個表空間中的審計跟蹤信息不會被壓縮,咱們只是向其中插入信息。
一個只讀表空間,其中包含「當前這一年」的審計跟蹤信息分區,在此採用一種壓縮格式。在每月的月初,置這個表空間爲可讀寫,向這個表空間中移入上個月的審計信息,並進行壓縮,再使之成爲只讀表空間,並完成備份。
用於去年、前年等的一系列表空間。這些都是隻讀表空間,甚至能夠放在很慢的廉價存儲介質上。若是出現介質故障,咱們只須要從備份恢復。有時能夠隨機地從備份集中選擇每年的信息,確保這些信息是可恢復的(有時磁帶會出故障)。
採用這種方式,就能很容易地完成淨化(即刪除一個分區)。一樣,歸檔也很輕鬆,只需先傳送一個表空間,之後再恢復。經過實現壓縮能夠減小空間的佔用。備份的 工做量會減小,由於在許多系統中,單個最大的數據集就是審計跟蹤數據。若是能夠從天天的備份中去掉某些或所有審計跟蹤數據,可能會帶來顯著的差異。
簡單地說,審計跟蹤需求和分區這兩個方面是緊密相關的,而不論底層系統是何種類型(數據倉庫或是OLTP系統)。