分区(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系统)。