为什么有些12月日历表表不是7列的,而是9列的

在这一章中我们将讨论各种类型的数据库表,并介绍什么情况下想用哪种类型的数据库表(也就是说在哪些情况下某种类型的表比其他类型更适用)。我们会强调表嘚物理存储特征:即数据如何组织和存储

从前只有一种类型的表,这千真万确原先确实只有一种“普通”表。管理这种表就像管理“┅个堆”一样(下一节会给出有关的定义)后来,Oracle又增加了几类更复杂的表如今,除了堆组织表外还有聚簇表(共有3种类型的聚簇表)、索引组织表、嵌套表、临时表和对象表。每种类型的表都有不同的特征因此分别适用于不同的应用领域。

在深入讨论细节之前峩们先对各种类型的表给出定义。Oracle中主要有9种表类型:

table):这些就是“普通”的标准数据库表数据以堆的方式管理。增加数据时会使鼡段中找到的第一个能放下此数据的自由空间。从表中删除数据时则允许以后的INSERT和UPDATE重用这部分空间。这就是这种表类型中的“堆”这个洺词的由来堆(heap)是一组空间,以一种有些随机的方式使用

table):这些表按索引结构存储。这就强制要求行本身有某种物理顺序在堆Φ,只要放得下数据可以放在任何位置;而索引组织表(IOT)有所不同,在IOT中数据要根据主键有序地存储。

table):聚簇(cluster)是指一个或多個表组成的组这些表物理地存储在相同的数据库块上,有相同聚簇键值的所有行会相邻地物理存储这种结构可以实现两个目标。首先多个表可以物理地存储在一起。一般而言你可能认为一个表的数据就在一个数据库块上,但是对于聚簇表可能把多个表的数据存储茬同一个块上。其次包含相同聚簇键值(如DEPTNO=10)的所有数据会物理地存储在一起。这些数据按聚簇键值“聚簇”在一起聚簇键使用B*树索引建立。

table):这些表类似于聚簇表但是不使用B*树索引聚簇键来定位数据,散列聚簇将键散列到聚簇上从而找到数据应该在哪个数据库塊上。在散列聚簇中数据就是索引(这是隐喻的说法)。如果需要频繁地通过键的相等性比较来读取数据散列聚簇表就很适用。

10g中新增的它结合了散列聚簇表的某些方面,同时兼有IOT的一些方面其概念如下:你的行按某个键值(如CUSTOMER_ID)散列,而与该键相关的一系列记录鉯某种有序顺序到达(因此这些记录是基于时间戳的记录)并按这种有序顺序处理。例如客户在你的订单输入系统中下订单,这些订單会按先进先出(first in, first out, FIFO)的方式获取和处理在这样一个系统中,有序散列聚簇就是适用的数据结构

table):嵌套表是Oracle对象关系扩展的一部分。咜们实际上就是系统生成和维护的父/子关系中的子表嵌套表的工作类似于SCOTT模式中的EMP和DEPT。可以认为EMP是DEPT表的子表因为EMP表有一个指向DEPT的外键DEPTNO。嵌套表与子表的主要区别是:嵌套表不像子表(如EMP)那样是“独立”表

table):这些表存储的是事务期间或会话期间的“草稿”数据。临時表要根据需要从当前用户的临时表空间分配临时区段每个会话只能看到这个会话分配的区段;它从不会看到其他任何会话中创建的任哬数据。

table):对象表基于某种对象类型创建它们拥有非对象表所没有的特殊属性,如系统会为对象表的每一行生成REF(对象标识符)对潒表实际上是堆组织表、索引组织表和临时表的特例,还可以包含嵌套表作为其结构的一部分

table):这些表并不存储在数据库本身中,而昰放在数据库之外即放在平常的操作系统文件中。在Oracle9i及以上版本中利用外部表可以查询数据库之外的一个文件,就好像这个文件也是數据库中平常的表一样外部表对于向数据库加载数据最有用(外部表是非常强大的数据加载工具)。Oracle 10g则更进一步还引入了一个外部表卸载功能,在不使用数据库链接的情况下这为在Oracle数据库之间移动数据提供了一种简单的方法。我们将在第15章更详细地讨论外部表

q         一个表最多可以有1000列,不过我不鼓励设计中真的包含这么多列除非存在某个硬性需求。表中的列数远远少于1000列时才最有效Oracle在内部会把列数夶于254的行存储在多个单独的行段(row piece)中,这些行段相互指向而且必须重新组装为完整的行影像。

q         表的行数几乎是无限的不过你可能会遇到另外某个限制,使得这种“无限”并不实际例如,一般来讲一个表空间最多有1022个文件(不过,Oracle 10g中有一些新的BIGFILE表空间这些表空间鈳以超出上述文件大小限制)。假设你有一些32GB的文件也就是说,每个表空间有32,704GB就会有2,143,289,344个块,每个块大小为16KB你可能在每个块上放160行(烸行大约80~100字节)。这样就会有342,926,295,040行不过,如果对这个表分区这个行数还能很容易地翻倍。例如假设一个表有1024个散列分区,则能有6,295,040行确实存在着上限,但是在接近这些上限之前你肯定会遇到另外某个实际限制。

q         表中的列有多少种排列(以及这些列的函数有多少种排列)表就可以有多少个索引。随着基于函数的索引的出现理论上讲,说能创建的索引数是无限的!不过同样由于存在一些实际的限淛,这会影响真正能创建和维护的索引数

q         即使在一个数据库中也可以有无限多个表。不过还是同样的道理,实际的限制会使数据库中嘚表数在一个合理的范围内不可能有数百万个表(这么多表对于创建和管理来说都是不实际的),但是有数千个表还是允许的

在下一節中,我们将讨论与表相关的一些参数和术语在此之后,我们再转而讨论基本的堆组织表然后介绍其他类型的表。

在这一节中我们將介绍与表相关的各种存储参数和术语。并非每种表类型都会用到所有参数例如,PCTUSED参数在IOT环境中就没有意义具体讨论各种表类型时还會分别介绍与之相关的参数。这一节的目标时介绍这些术语并给出定义。在后面几节中还会在适当的时候介绍使用特定参数的更多信息。

Oracle 中的段(segment)是占用磁盘上存储空间的一个对象尽管有多种类型,不过最常见的段类型如下:

聚簇(cluster):这种段类型能存储表有两種类型的聚簇:B*树聚簇和散列聚簇。聚簇通常用于存储多个表上的相关数据将其“预联结”存储到同一个数据库块上;还可以用于存储┅个表的相关信息。“聚簇”这个词是指这个段能把相关的信息物理的聚在一起

partition segment)类型——不论出于什么原因,Oracle将分区lobindex标记为一个索引汾区(有人很奇怪为什么要另外给lobindex取一个特殊的名字!)

举例来说,一个表可以是一个段索引有可能是一个段。这里我强调了“可能”这是因为,我们可以把一个索引划分到不同的段中所以,索引对象本身只是一个定义而不是一个物理段,索引可能由多个索引分區组成而每个索引分区(index partition)是一个段。表可能是一个段也可能不是。由于同样的原因由于表分区,一个表可以有多个表段:或者可鉯在一个称为聚簇的段中创建一个表此时这个表可能与其他表同在一个聚簇段中。

不过最常见的情况是,表是一个段索引也是一个段。对现在来说这样考虑最简单。创建一个表时通常就是创建一个新的表段,而且如第3章所述这个段包含区段,区段则包含块这昰平常的存储层次结构。但是要指出重要的一点只在“通常”情况下才有这种一对一的关系。例如考虑以下这个简单的CREATE TABLE语句:

这个语呴创建6个段。如果在一个初始为空(什么也没有)的模式中发出下面的CREATE TABLE语句会观察到以下结果:

在这个例子中,表本身创建了一个段:洳输出中最后一行所示这里主键约束创建了一个索引段,以保证惟一性

注意      惟一约束或主键可能创建一个新索引,也可能不创建如果约束列上已经有一个索引,而且这些列处于索引的前几列这个约束就会(而且将会)使用这些列(而不再创建创建新索引)。

BLOB)所指嘚实际数据块另一个段用于“组织”这些数据块。LOB为非常大块的信息提供了支持可以多达几GB。LOB存储在lobsegment的块中lobindex用于跟踪这些LOB块在哪里,以及应该以何种顺序来访问它们

从Oracle 9i开始,管理段空间有两种方法:

GROUPS、PCTUSED和其他参数来控制如何分配、使用和重用段中的空间在这一章Φ我会把这种空间管理方法称为MSSM,但是要记住这是一个我自造的缩写,Oracle文档中没有这个缩写

1中才首次引入。原先用于控制空间分配和提供高并发性的参数数不胜数并且需要对这些参数进行调整,人们不希望还要这么做这正是设计ASSM的出发点。例如倘若将FREELISTS参数设置为默认值1,可能会出现如果你的段是插入/更新新密集的(有大量插入/更新操作),对自由空间的分配就会存在竞争Oracle要在表中插入一行,戓更新一个索引键条目或者由于更新一行而导致这一行迁移时(稍后还会更多地介绍这方面的内容),可能需要从与这个段相关的自由塊列表中得到一个块如果只有一个自由块列表,一次就只有一个事务能查看和修改这个列表事务之间必须相互等待。在这种情况下洳果有多个FREELISTS和FREELIST GROUPS,就能提高并发性因为事务可以分别查看不同的列表,而不会相互竞争

稍后讨论存储设置时,我还会提到哪些参数用于掱工段空间管理而哪些参数用于自动段空间管理,不过需要指出在存储/段特征这方面,应用于ASSM段的存储设置只有:

其他存储和物理属性参数都不适用于ASSM段

段空间管理是从段的表空间(而且段从不会跨表空间)继承来的一个属性。段要使用ASSM就必须位于支持ASSM空间管理的表空间中。

存储在数据库中的表段使用了这个术语例如,如果把表想象成一个“平面”结构或者想象成从左到右依次排开的一系列块,高水平线(high-water markHWM)就是包含了数据的最右边的块,如图10-1所示

图10-1显示了HWM首先位于新创建表的第一个块中。过一段时间后随着在这个表中放入数据,而且使用了越来越多的块HWM会升高。如果我们删除了表中的一些(甚至全部)行可能就会有许多块不再包含数据,但是咜们仍在HWM之下而且这些块会一直保持在HWM之下,直到重建、截除或收缩这个对象(将段收缩是Oracle 10g的一个新特性只有当段在一个ASSM表空间中时財支持这个特性)。

HWM很重要因为Oracle在全面扫描段时会扫描HWM之下的所有块,即使其中不包含任何数据这会影响全面扫描的性能,特别是当HWMの下的大多数块都为空时要查看这种情况,只需创建一个有1,000,000行的表(或者创建其他有大量行的表)然后对这个表执行一个SELECT COUNT(*)。下面再删除(DELETE)这个表中的每一行你会发现尽管SELECT COUNT(*)统计出0行,但是它与统计出1,000,000所花的时间一样长(如果需要完成块清除时间可能还会更长:有关內容请参加9.5.5节)。这是因为Oracle在忙于读取HWM之下的所有块查看其中是否包含数据。如果对这个表使用TRUNCATE而不是删除其中的每一行你可以比较┅下结果有什么不同。TRUNCATE会把表的HWM重置回“0”还会截除表上的相关索引。由于以上原因如果你打算删除表中的所有行,就应该选择使用TRUNCATE(如果可以使用的话)

在一个MSSM表空间中,段只有一个HWM不过,在ASSM表空间中除了一个HWM外,还有一个低HWM(见图10-2)在MSSM中,HWM推进时(例如插入行时),所有块都会并立即有效Oracle可以安全地读取这些块。不过对于ASSM,HWM推进时Oracle并不会立即格式化所有块,只有在第一次使用这些块时才会完成格式化以便安全地读取。所以全面扫描一个段时,必须知道要读取的块是否“安全”或是否格式化(这说明,其中鈈包含有意义的信息不能对其进行处理)。为了避免表中每一个块都必须经过这种安全/不安全检查Oracle同时维护了一个低HWM和一个HWM。Oracle会全表掃描至HWM对于低HWM以下的所有块会直接读取并加以处理。而对介于低HWM和HWM之间的块则必须更加小心,需要参考管理这些块所用的ASSM位图信息来查看应该读取哪些块而哪些块应该被忽略。

使用MSSM表空间时Oracle会在自由列表(freelist)中为有自由空间的对象维护HWM一些的块。

每个对象都至少有┅个相关的freelist使用块时,可能会根据需要把块放在freelist上或者从freelist删除需要说明的重要一点是,只有位于HWM以下的对象块才会出现在freelist中仅当freelist为涳时才会使用HWM之上的块,此时Oracle会推进HWM并把这些块增加到freelist中,采用这种方式Oracle会延迟到不得已时才增加对象的HWM。

一个对象可以有多个freelist如果预计到会有多个并发用户在一个对象上执行大量的INSERT或UPDATE活动,就可以配置多个freelist这对性能提升很有好处(但是可能要以额外的存储空间为玳价)。根据需要配置足够多的freelist非常重要

如果存在多个并发的插入和更新,在这样一个环境中FREELISTS可能对性能产生巨大的影响(可能是促進,也可能是妨碍)通过一个极其简单的测试就能看出正确地设置FREELISTS有什么好处。请考虑下面这个相对简单的表:

接下来使用5个并发会话开始“疯狂地”对这个表执行插入。如果分别测量插入前和插入后与块相关的系统级等待事件就会发现长时间的等待,特别是对数据塊的等待(试图插入数据)这通常是因为表(以及索引)上的freelist不足造成的(不过关于索引的有关内容将在下一章更详细介绍)。为此我使用了Statspack首先取一个statspace.snap,接下来执行一个脚本开始5个并发的SQL*Plus会话等这些会话退出后再取另一个statspace.snap。这些会话运行的脚本很简单如下:

这是┅个非常简单的代码块,此时我是数据库中惟一的用户按理说,应该得到最佳的性能因为我配置了充足的缓冲区缓存,重做日志大小佷合适另外索引也不会减慢速度,而且这是在有两个超线程Xeon CPU的主机上运行这个主机应该能运行得很快。不过我看到的结果却是:

对buffer busy waits總共等待了119秒,也就是每个会话大约24秒导致这些等待的原因完全是:表中没有配置足够的freelist来应付发生的这种并发活动。不过只需将表創建为有多个freelist,就能轻松地消除大部分等待时间:

或者也可以通过修改对象达到目的:

你会看到buffer busy waits大幅下降,而且所需的CPU时间也随着耗用時间的下降而减少(因为这里做的工作更少;对闩定数据结构的竞争确实会让CPU焦头烂额):

对于一个表来说你可能想确定最多能有多少個真正的并发插入或更新(这需要更多空间)。这里我所说的“真正的并发”是指你认为两个人在同一时刻请求表中一个自由块的情况昰否频繁。这不是对重叠事务的一种量度;而是量度多少个会话在同时完成插入而不论事务边界是什么。你可能希望对表的并发插入有哆少freelist就有多少,以此来提高并发性

只需把freelist设置得相当高,然后就万事大吉了是这样吗?当然不是哪有这么容易。使用多个freelist时有┅个主freelist,还有一些进程freelist如果一个段只有一个freelist,那么主freelist和进程freelist就是这同一个自由列表如果你有两个freelist,实际上将有一个主freelist和两个进程freelist对於一个给定的会话,会根据其会话ID的散列值为之指定一个进程freelist目前,每个进程freelist都只有很少的块余下的自由块都在主freelist上。使用一个进程freelist時它会根据需要从主freelist拉出一些块。如果主freelist无法满足空间需求Oracle就会推进HWM,并向主freelist中增加空块过一段时间后,主freelist会把其存储空间分配多個进程freelist(再次说明每个进程freelist都只有为数不多的块)。因此每个进程会使用一个进程freelist。它不会从一个进程freelist到另一个进程freelist上寻找空间这說明,如果一个表上有10个进程freelist而且你的进程所用的进程freelist已经用尽了该列表中的自由缓冲区,它不会到另一个进程freelist上寻找空间即使另外9個进程freelist都分别有5块(总共有45个块),此时它还是会去求助主freelist假设主freelist上的空间无法满足这样一个自由块请求,就会导致表推进HWM或者如果表的HWM无法推进(所有空间都已用),就要扩展表的空间(得到另一个区段)然后这个进程仍然只使用其freelist上的空间(现在不再为空)。使鼡多个freelist时要有所权衡一方面,使用多个freelist可以大幅度提升性能另一方面,有可能导致表不太必要地使用稍多的磁盘空间你必须想清楚茬你的环境中哪种做法麻烦比较小。

8.1.6及以后版本中你可以根据意愿自由地将其改大或改小。可以把它修改为一个大数从而与采用传统蕗径模式的SQL*Loader并行完成数据的加载。这样可以获得高度并发的加载而只有最少的等待。加载之后可以再把这个值降低为某个更合理的平瑺的数。将空间改小时现有的多个freelist上的块要合并为一个主freelist。

要解决前面提到的缓冲区忙等待问题还有一种方法,这就是使用一个ASSM管理嘚表空间还是前面的例子,但在ASSM管理的表空间中要如下创建表T:

你会看到在这种情况下,缓冲区忙等待、CPU时间和耗用时间都会下降茬此不必确定最好要有多少个freelist:

这就是ASSM的主要作用之一:不必手动地确定许多关键存储参数的正确设置。

一般而言PCTFREE参数用来告诉Oracle应该在塊上保留多少空间来完成将来的更新。默认情况下这个值是10%。如果自由空间的百分比高于PCTFREE中的指定值这个块就认为是“自由的”。PCTUSED則告诉Oracle当前不“自由”的一个块上自由空间百分比需要达到多大才能使它再次变为自由的默认值是40%1

如前所述对于一个表(而不是┅个IOT,有关内容稍后再介绍)PCTFREE会告诉Oracle:块上应该保留多大的空间来完成将来的更新。这说明如果我们使用的块大小为8KB,只要向块中增加一个新行就会导致块上的自由空间下降大约800字节,Oracle会使用FREELIST的另一个块而不是现有的块。块上这10%的数据空间会预留出来以便更新該块上的行。

实际上PCTUSED的含义是如果块上不自由的空间到达或小于PCTUSED参数指定的百分比时,这个块将重新变为自由如倘若PCTUSED为40%,那么块上鈈自由的空间小于40%时即自由空间达到60%时,这个块就重新变为自由——译者注。

对于不同的表类型PCTFREE和PCTUSED的实现有所不同。对于某些表类型这两个参数都要使用,而另外一些表类型只使用PCTFREE而且对于这些表类型,仅当创建对象时才会使用PCTFREEIOT在创建时可以使用PCTFREE在表中预覽空间来完成将来的更新,但是在其他方面并不使用PCTFREE例如,PCTFREE不用于决定何时停止向一个给定块中插入行

根据你使用的是ASSM表空间还是MSSM表涳间,这两个参数的实际作用会有所不同使用MSSM时,这些参数设置控制着块何时放入freelist中以及何时从freelist中取出。如果使用默认值:PCTFREE为10PCTUSED为40,那么在块到达90%满之前(有10%以上的自由空间)这个块会一直在freelist上。一旦到底90%就会从freelist中取出,而且直到块上的自由空间超过了块的60%时才會重新回到freelist上,在此之前这个块一直不在freelist上。

使用ASSM时PCTFREE仍然会限制能否将一个新行插入到一个块中,但是它不会控制一个块是否在freelist上洇为ASSM根本不使用freelist。在ASSM中PCTUSED将被忽略。

PCTFREE 有3种设置:太高、太低好刚好如果把块的PCTFREE设置得过高,就会浪费空间如果把PCTFREE设置为50%,而你从未更噺数据那么每个块都会浪费50%的空间。不过在另一个表上,50%可能非常合理如果行初始很小,现在想将行的大小加倍但是倘若PCTFREE设置得呔小,更新行时就会导致行迁移

到底什么是行迁移?行迁移(row migration)是指由于某一行变得太大无法再与其余的行一同放在创建这一行的块Φ(块中已经放不下这一行),这就要求这一行离开原来的块这一节将分析行迁移。首先来看一个块如同10-3所示。

图10-3 更新前的数据块

这個块上大约1/7是自由空间不过,我们想通过一个UPDATE将第4行所有的空间加倍(第4行现在占用了块上1/7的空间)在这种情况下,即使Oracle合并了块上嘚空间(如同10-4所示)还是没有足够的空间将第4行的大小加倍,因为自由空间小于第4行的当前大小

如果这一行能在合并的空间中放下,洎然就会这么做不过,在此Oracle没有完成这个合并块还是保持原样。因为第4行如果还呆在这个块上它就必须跨块,所以Oracle会移动或迁移这┅行不过,Oracle不能简单地移动这一行它必须留下一个“转发地址”。可能有一些索引物理地指向第4行的这个地址简单的更新不会同时修改这些索引(注意对于分区表则有一个特例:更新分区表时,rowid即行地址会改变这种情况将在第13章介绍)。因此Oracle迁移这一行时,它会留下一个指针指示这一行实际上在什么位置。更新之后块可能如图10-5所示。

row)就是这一行从最初所插入的块上移到另外的某个块上为什么这会带来问题?你的应用绝对不会知道存在行迁移;你使用的SQL也没有任何不同行迁移只会影响性能。如果你通过一个索引来读这一荇索引会指向原来的块,那个块再指向这个新块要得到具体的行数据,一般并不是执行两个左右的I/O就可以得到行数据单独来看,这鈈是大问题甚至根本注意不到。不过如果这种行所占的比例相当大,而且有大量用户在访问这些行你就会注意到这种副作用了。访問这些数据的速度开始变慢(额外的I/O以及与I/O相关的闩定都会增加访问时间)缓冲区缓存的效率开始下降(需要缓存两个块,而如果行没囿迁移就只需要缓存一个块)另外表的大小好复杂性都有所增加。由于这些原因你可能不希望迁移行。

有意思的是如果一行从左边嘚块迁移到右边的块,如同10-5所示而且它在将来某个时间点还要再迁移,Oracle会这样做呢造成这种又一次迁移的原因可能是:在这一行迁移箌的“目标”块上又增加了其他的行,然后这一行再次更新变得更大。Oracle实际上会把这一行迁移回原来的块如果有足够的空间,仍放回原地(这么一来这一行可能变得“未迁移”)。如果没有足够的空间Oracle会把这一行迁移到另外的某个块上,并修改原来块上的转发地址因此,行迁移总是涉及一层间接性

所以,现在我们再回到PCTFREE来说明这个参数的作用:如果设置得当,这个参数可以帮助你尽量减少行串链

设置PCTFREE和PCTUSED是一个很重要的主题,不过往往被忽视总的来说,使用MSSM时PCTUSED和PCTFREE都很重要;对于ASSM,只有PCTFREE是重要的一方面,你要使用这些参數来避免迁移过多的行另一方面,要使用这些参数避免浪费太多的空间你需要查看对象,描述这些对象要如何使用然后为设置这些徝得出一个逻辑计划。设置这些参数时如果主观地采用一般经验很可能招致失败;必须根据具体的使用设置。可以考虑以下做法(要记住这里的“高”和“低”都是相对的;而且使用ASSM时仅PCTFREE适用):

高PCTFREE,低PCTUSED:如果你插入了将要更新的大量数据而且这些更新会频繁地增加荇的大小,此时就适合采用这种设置这种设置在插入后会在块上预留大量的空间(高PCTFREE),并使得将块放回到freelist之前必须几乎为空(低PCTUSED)

通常对象都采用LOGGING方式创建,这说明对象上完成的操作只要能生成redo就都会生成redoNOLOGGING则允许该对象完成某些操作时可以不生成redo;这个内容在上一嶂详细介绍过。NOLOGGING只影响几个特定的操作如对象的初始创建,或使用SQL*Loader的直接路径加载或者重建(请参考Oracle SQL Reference手册来了解你使用的数据库对象鈳以应用哪些操作)。

这个选项并不会完全禁用对象的重做日志生成只是几个特定的操作不生成日志而已。例如如果把一个表创建为SELECT NOLOGGING,然后INSERT INTO THAT_TABLE VALUES(1)这个INSERT就会生成日志,但是表创建可能不生成redo(DBA可以在数据库或表空间级强制生成日志)

段中每个块都有一个块首部。这个块首蔀中有一个事务表事务表中会建立一些条目来描述哪些事务将块上的哪些行/元素锁定。这个事务表的初始大小由对象的INITRANS设置指定对于表,这个值默认为2(索引的INITRANS也默认为2)事务表会根据需要动态扩展,最大达到MAXTRANS个条目(假设块上有足够的自由空间)所分配的每个事務条目需要占用块首部中的23~24字节的存储空间。注意对于Oracle

应用中99%(或者更多)的情况下使用的可能都是堆组织表,不过随着IOT的出现这種状况以后可能会有所改观,因为IOT本身就可以加索引执行CREATE TABLE语句时,默认得到的表类型就是堆组织表如果你想要任何其他类型的表结构,就需要在CREATE语句本身中指定它

堆(heap)是计算机科学领域中得到深入研究的一种经典数据结构。它实际上就是一个很大的空间、磁盘或内存区(当然这里所说的磁盘是指数据库表的相应磁盘),会以一种显然随机的方式管理数据会放在最合适的地方,而不是以某种特定順序来放置许多人希望能按数据放入表中的顺序从表中取出数据,但是对于堆这是无法保证的。这一点很容易说清楚

在以下的例子Φ,我将建立一个表使得在我的数据库中每块刚好能放一个整行(我使用的块大小是8KB)。不一定非得每块上有一行我只是想利用这一點来展示一种可预测的事务序列。不论数据库使用多大的块大小也不论表的大小如何,都可以观察到以下行为(行没有次序):

如果你想再试试(得到同样的结果)可以根据你的块大小来调整B和C列。例如如果你的块大小为2KB,则不需要C列而且B列应该是一个VARCHAR2(1500),默认有1,500个煋号在这样一个表中,由于数据在堆中管理只要有空间变为可用,就会重用这个空间

注意      使用ASSM或MSSM时,你会发现行最后会在“不同的位置上”底层的空间管理例程有很大差别,在ASSM和MSSM中对同一个表执行同样的操作很可能得到不同的物理顺序。尽管数据逻辑是相同的泹是它们会以不同的方式存储。

全部扫描时会按命中的顺序来获取数据,而不是以插入的顺序这是一个必须了解的重要的数据库表概念:一般来讲,数据库表本质上是无序的数据集合还应该注意到,要观察到这种效果不必在INSERT后接下来再使用DELETE;只需使用INSERT就可以得到同樣的结果。如果我插入一个小行那么观察到的结果很可能是:取出行时默认的顺序为“小行、小行、大行”。这些行并不按插入的顺序獲取Oracle会把数据放在能放下的任何地方,而不是按照日期或事务的某种顺序来存放

如果你的查询需要按插入的顺序来获取数据,就必须姠表中增加一列以便获取数据时使用这个列对数据排序。例如这可以是一个数字列,有一个递增的序列(使用Oracle SEQUENCE对象)只需使用一个SELECT,其ORDER BY子句对这个列完成排序这样就可以模拟插入顺序。这个顺序可能只是近似的因为序号为55的行很可能在序号为54的行之前提交,因此数据库中序号为55的行可能放在前面。

应该把堆组织表看作一个很大的无序行集合这些行会以一种看来随机的顺序取出,而且取出的顺序还取决于所用的其他选项(并行查询、不同的优化器模式等待),同一个查询可能会以不同的顺序取出数据不要过分依赖查询得到嘚顺序,除非查询中有一个ORDER BY语句!

除此之外关于堆表还有什么重要的内容需要了解?要知道Oracle SQL Reference手册中介绍CREATE TABLE语法时足足用了72页,所以有关嘚选项当然多由于存在如此之多的选项,所以很难全部掌握“线路图”(或“轨迹”图)本身就用了18页来介绍。要了解一个给定表的CREATE TABLE語句中主要有哪些可用的选项我用了一个技巧。首先尽可能简单地创建表,例如:

然后使用标准内置包DBMS_METADATA,查询这个表的定义并查看详细语法:

这个技巧的好处是,它显示了CREATE TABLE语句的许多选项我只需要提供数据类型,Oracle就会为我生成详细的“版本”(CREATE TABLE版本)现在我可鉯定制这个详细的版本,可能把ENABLE STORAGE IN ROW改成DISABLE STORAGE IN ROW这样会禁用随结构化数据在行中存储LOB数据,而把LOB数据存储在另外一个段中我一直都在使用这个技巧来节省我的时间,因为要从那个庞大的线路图中找出该使用哪个选项很让人犯愁如果不采用这个技巧,可能就会为此浪费好几分钟使用这个技术还可以了解不同的情况下CREATE TABLE语句有哪些可用的选项。

既然你知道了如何查看一个给定的CREATE TABLE语句可用的大多数选项那么对于堆表來说,需要注意哪些重要的选项呢在我看来,对于ASSM有两个重要选项对于MSSM,重要选项有4个:

FREELISTS :仅适用于MSSM每个表都会在一个freelist上管理堆中汾配的块。一个表可以有多个freelist如果你认定会有多个并发用户对表执行大量的插入,配置多个freelist可能会大大地改善性能(可能要以额外的存儲空间为代价)这个设置对性能可能产生的影响请参见“FREELISTS”一节中的讨论和有关例子。

PCTFREE :ASSM和MSSM都适用在INSERT过程中,会测量块的充满程度洳前所示,根据块当前充满的程度这个参数用于控制能否将一行增加到一个块上。这个选项还可以控制因后续更新所导致的行迁移要根据将如何使用表来适当地设置。

PCTUSED :仅适用于MSSM度量一个块必须为多空才允许再次插入行。如果块中已用的空间小于PCTUSED就可以插入新行了。同样地类似于PCTFREE,必须考虑你将如何使用表从而适当地设置这个选项。

INITRANS :ASSM和MSSM都适合为块初始分配的事务槽数。如果这个选项设置得呔低(默认值为2这也是最小值),可能导致多个用户访问的一个块上出现并发问题如果一个数据块机会已满,而且事务表无法动态扩展会话就会排队等待这个块,因为每个并发事务都需要一个事务槽如果你认为会对同样的块完成多个并发更新,就应该考虑增大这个徝

注意      单独存储在LOB段中的LOB数据并不使用表的PCTFREE/PCTUSED参数设置。这些LOB块以不同的方式管理:它们总是会填入直至达到最大容量,而且仅当完全為空时才返回freelist

这些参数要特别注意。随着本地管理表空间的引入(这也是强烈推荐的做法)我发现其余的参数(如PCTINCREASE、NEXT等)已经没有什麼意义了。

索引组织表(index organized tableIOT)就是存储在一个索引结构中的表。存储在堆中的表是无组织的(也就是说只要有可用的空间,数据可以放茬任何地方)IOT中的数据则按主键存储和排序。对你的应用来说IOT表现得与一个“常规”表并无二致;还是要使用SQL正常地访问这些表。IOT对信息获取、空间应用和OLAP应用特别有用

有什么意义?实际上可以反过来问:堆组织表有什么意义?由于一般认为关系数据库中的所有表嘟有一个主键堆组织表难道不是在浪费空间吗?使用堆组织表时我们必须为表和表主键上的索引分别留出空间。而IOT则不存在主键的空間开销因为索引就是数据,数据就是索引两者已经合二为一。事实上索引是一个复杂的数据结构,需要大量的工作来管理和维护洏且随着存储的行宽度有所增加,维护的需求也会增加另一方面,相比之下堆管理起来则很容易。对组织表在某些方面的效率要比IOT高一般认为,比起堆组织表来说IOT有一些突出的优点。例如记得曾经有一次,我在一些文本数据上建立一个反向表索引(那时还没有引叺interMedia和相关的技术)我有一个表,其中放满了文档并发现其中的单词。我的表如下所示:

在此我的表完全由主键组成。因此有超过100%的(主键索引)开销;表的大小与主键索引的大小相当(实际上主键索引更大,因为它物理地存储了所指向的行的rowid;而表中并不存储rowid表Φ的行ID是推断出来的)。使用这个表时WHERE子句只选择了WORD列或WORD和POSITION列。也就是说我并没有使用表,而只是使用了表上的索引表本身完全是開销。我想找出包含某个给定单词的所有文档(或者满足“接近”每个词等匹配条件)此时,堆表是没有用的它只会在维护KEYWORDS表时让应鼡变慢,并使存储空间的需求加倍这个应用就非常适合采用IOT。

另一个适于使用IOT的实现是代码查找表例如,可能要从ZIP_CODE查找STATE此时可以不偠堆表,而只使用IOT本身如果你只会通过主键来访问一个表,这个表就非常适合实现为IOT

如果你想保证数据存储在某个位置上,或者希望數据以某种特定的顺序物理存储IOT就是一种合适的结构。如果是Sybase和SQL Server的用户你可能会使用一个聚簇索引,但是IOT比聚簇索引更好这些数据庫中的聚簇索引可能有多达110%的开销(与前面的KEYWORDS表例子类似)。而使用IOT的话我们的开销则是0%,因为数据只存储一次有些情况下,你可能唏望数据像这样物理地共同存储在一处父/子关系就是这样一个典型的例子。假设EMP表有一个包含地址的子表员工最初递交求职信时,你鈳能向系统中(地址比表中)输出一个家庭地址过一段时间后,他搬家了就要把家庭地址修改为原地址,并增加一个新的家庭地址嘫后,他可能还会回去读学位此时可能还要增加一个学校地址,等等也就是说,这个员工有3~4个(或者更多)的(地址)详细记录泹是这些详细记录是随机到来的。在一个普通的基于堆的表中这些记录可以放在任何地方。两个或更多地址记录放在堆表的同一个数据庫块上的概率接近于0.不过你查询员工的信息时,总会把所有地址详细记录都取出来在一段时间内分别到达的这些行总会被一并获取得箌。为了让这种获取更为高效可以对子表使用IOT,使得子表将对应某个给定员工的所有记录都插入到相互“靠近”的地方这样在反复获取这些记录时,就可以减少工作量

使用一个IOT将子表信息物理地存储在同一个位置上有什么作用?这一点通过一个例子就能很容易地说明下面创建并填充一个EMP表:

接下来,将这个子表实现两次:一次作为传统的堆表另一次实现为IOT:

我如下填充这些表,首先为每个员工插叺一个工作地址其次插入一个家庭地址,再次是原地址最后是一个学校地址。堆表很可能把数据放在表的“最后”;数据到来时堆表只是把它增加到最后,因为此时只有数据到来而没有数据被删除。过一段时间后如果有地址被删除,插入就开始变得更为随机会隨机地插入到整个表中的每个位置上。不过有一点是肯定的,堆表中员工的工作地址与家庭地址同在一个块上的机率几乎为0.不过对于IOT,由于键在EMPNOADDR_TYPE上,完全可以相信:对应一个给定EMPNO的所有地址都会放在同一个(或者两个)索引块上填充这些数据的插入语句如下:

我把這个插入又做了3次,依次将WORK分别改为HOME、PREV和SCHOOL然后收集统计信息:

现在可以看看我们预料到的显著差别。通过使用AUTOTRACE可以了解到改变有多大:

这是一个相对常见的计划:按主键访问EMP表;得到行;然后使用这个EMPNO访问地址表;接下来使用索引找出子记录。获取这个数据执行了11次I/O丅面再运行同样的查询,不过这一次地址表实现为IOT:

这里少做了4次I/O(这个4应该能推测出来);我们跳过了4个TABLE ACCESS (BY INDEX ROWID)步骤子表记录越多,所能跳過的I/O就越多

那么,这4个I/O是什么呢在这个例子中,这是查询所完成I/O的1/3还多如果反复执行这个查询,这就会累积起来每个I/O和每个一致獲取需要访问缓冲区缓存,尽管从缓存区缓存读数据要比从磁盘读快得多但是要知道,缓存区缓存获取并不是“免费”的而且也绝对鈈是“廉价”的。每个缓冲区缓存获取都需要缓冲区缓存的多个闩而闩是串行化设备,会限制我们的扩展能力通过运行以下PL/SQL块,可以測量出I/O和闩定的减少:

这里只是模拟我们很忙在此将查询运行大约45,000次,对应各个EMPNO运行一次如果对HEAP_ADRESSES和IOT_ADDRESSES表分别运行这个代码,TKPROF会显示如下結果:

0

0

两个查询获取的行数同样多但是HEAP表完成的逻辑I/O显著增加。随着系统并发度的增加可以想见,堆表使用的CPU时间也会增长得更快洏查询耗费CPU时间的原因可能只是在等待缓冲区缓存的闩。使用runstats(我自己设计的一个工具)可以测量出两种实现的闩定之差。在我的系统仩观察到的结果是:

在此Run1是HEAP_ADDRESSES表,Run2是IOT_ADDRESSES表可以看到,在发生的闩定方面存在显著的下降,而且这种下降可以重复验证这主要是因为缓沖区缓存存在闩的串链(即保护缓冲区缓存的闩)。在这种情况下IOT提供了以下好处:

q         每个查询完成的物理I/O更少,因为对于任何给定的查詢需要的块更少,而且对地址记录的一个物理I/O很可能可以获取所有地址(而不只是其中一个地址但堆表实现就只是获取一个地址)。

洳果经常在一个主键或惟一键上使用BETWEEN查询也是如此。如果数据有序地物理存储就能提升这些查询的性能。例如我在数据库中维护了┅个股价表。每天我要收集数百支股票的股价记录、日期、收盘价、当日最高价、当日最低价、买入卖出量和其他相关信息这个表如下所示:

我经常一次查看一支股票几天内的表现(例如,计算移动平均数)如果我使用一个堆组织表,那么对于股票记录ORCL的两行在同一个數据库块上的可能性几乎为0.这是因为每天晚上我都会插入当天所有股票的记录。这至少会填满一个数据库块(实际上可能会填满多个數据库块)。因此每天我都会增加一个新的ORCL记录,但是它总在另一个块上与表中已有的其他ORCL记录不在同一个块上。如果执行如下查询:

会读取索引然后按rowid来访问表,得到余下的行数据由于我加载表所采用的方式,获取的每100行会在一个不同的数据库块上所有每获取100荇可能就是一个物理I/O。下面考虑IOT中有同样的数据这是这个查询,不过现在只需要读取相关的索引块这个索引块中已经有所有的数据。茬此不仅不存在表访问而且一段时期内对于ORCL的所有行物理存储在相互“邻近”的位置。因此引入的逻辑I/O和物理I/O都更少

现在你已经知道叻什么时候想使用IOT,以及如何使用IOT接下来需要了解这些表有哪些选项。有哪些需要告诫的方面IOT的选项与堆组织表的选项非常相似。我們还是使用DBMS_METADATA来显示详细选项先从IOT的3个基本变体开始:

后面会介绍OVERFLOW和INCLUDING会为我们做什么,不过首先来看第一个所需的详细SQL:

这个表引入了两個新的选项:NOCOMPRESS和PCTTHRESHOLD稍后将介绍它们。你可能已经注意到了与前面的CREATE TABLE语法相比,这里好像少了点什么:没有PCTUSED子句但是这里有一个PCTFREE。这是洇为索引是一个复杂的数据结构,它不像堆那样随机组织所以数据必须按部就班地存放到它该去的地方去。在堆中块只是有时能插叺新行,而索引则不同块总是可以插入新的索引条目。如果每个数据(根据它的值)属于一个给定块在总会放在那个块上,而不论这個块多满或者多空另外,只是在索引结构中创建对象和填充数据时才会使用PCTFREE其用法与堆组织表中的用法不同。PCTFREE会在新创建的索引上预留空间但是对于以后对索引的操作不预留空间,这与不使用PCTUSED的原因是一样的堆组织表上关于freelist的考虑同样完全适用于IOT。

现在来讨论新发現的选项NOCOMPRESS这个选项对索引一般都可用。它告诉Oracle把每个值分别存储在各个索引条目中(也就是不压缩)如果对象的主键在A、B和C列上,A、B囷C的每一次出现都会物理地存储NOCOMPRESS反过来就是COMPRESS N,在此N是一个整数表示要压缩的列数。这样可以避免重复值并在块级提取“公因子”(factor out)。这样在A的值(以及B的值)重复出现时将不再物理地存储它们。例如请考虑如下创建的一个表:

可以想想看,每个模式(作为OWNER)都擁有大量对象所有OWNER值可能会重复数百次。甚至OWNEROBJECT_TYPE值对也会重复多次,因为给定模式可能有数十个表、数十个包等只是这3列合在一起不會重复。可以让Oracle压缩这些重复的值索引块不是包含表10-1所示的值,而是可以使用COMPRESS 2(提取前两列)包含表10-2所示的值。

也就是说值SYS和TABLE只出現一次,然后存储第三列采用这种方式,每个索引块可以有更多的条目(否则这是不可能的)这不会降低并发性,因为我们仍在行级操作;另外也不会影响功能它可能会稍微多占用一些CPU时间,因为Oracle必须做更多的工作将键合并在一起另一方面,这可能会显著地减少I/O並允许更多的数据在缓冲区缓存中缓存,原因是每个块上能有更多的数据这笔交易很划得来。

现在可以测量所用的空间为此我们将使鼡ANALYZE INDEX VALIDATE STRUCTURE命令。这个命令会填写一个名为INDEX_STATS的动态性能视图其中最多只包含一行,即这个ANALYZE命令最后一次执行的信息:

由此显示出我们的索引目湔使用了284个叶子块(即数据所在的块),并使用了3个分支块(Oracle在索引结构中导航所用的块)来找到这些叶子块使用的空间大约是2MB(2,038,248字节)。另外两列名字有些奇怪这两列是要告诉我们一些信息。OPT_CMPR_COUNT(最优压缩数)列要说的是:“如果你把这个索引置为COMPRESS 2就会得到最佳的压縮”。OPT_CMPR_PCTSAVE(最优的节省压缩百分比)则是说如果执行COMPRESS 2,就能节省大约1/3的存储空间索引只会使用现在2/3的磁盘空间。

为了测试上述理论我們先用COMPRESS 1重建这个IOT:

可以看到,索引确实更小了:大约1.7MB叶子块和分支块都更少。但是现在它说“你还能再节省另外23%的空间”,因为我们沒有充分地压缩下面用COMPRESS 2再来重建IOT:

0

现在大小有了显著减少,不论是叶子块数还是总的使用空间都大幅下降现在使用的空间大约是1.3MB。再來看原来的数字:

可以看到OPT_CMPR_PCTSAVE真是精准无比上一个例子指出,关于IOT有一点很有意思:IOT是表但是只是有其名而无其实。IOT段实际上是一个索引段

现在我先不讨论PCTTHRESHOLD选项,因为它与IOT的下面两个选项有关:OVERFLOW和INCLUDING如果查看以下两组表(T2和T3)的完整SQL,可以看到如下内容(这里我使用了┅个DBMS_METADATA例程来避免STORAGE子句因为它们对这个例子没有意义):

所以,现在只剩下PCTTHRESHOLD、OVERFLOW和INCLUDING还没有讨论这三个选项有点“绕”,其目标是让索引叶孓块(包含具体索引数据的块)能够高效地存储数据索引一般在一个列子集上。通常索引块上的行数比堆表块上的行数会多出几倍索引指望这每块能得到多行。否则Oracle会花费大量的时间来维护索引,因为每个INSERT或UPDATE都可能导致索引块分解以容纳新数据。

OVERFLOW 子句允许你建立另┅个段(这就使得IOT成为一个多段对象就像有一个CLOB列一样),如果IOT的行数据变得太大就可以溢出到这个段中。

注意使用MSSM时,OVERFLOW再次为IOT引叺了PCTUSED子句对于OVERFLOW段和堆表来说,PCTFREE和PCTUSED的含义都相同使用溢出段的条件可以采用两种方式来指定:

PCTTHRESHOLD:行中的数据量超过块的这个百分比时,荇中余下的列将存储在溢出段中所以,如果PCTTHRESHOLD是10%而块大小是8KB,长度大于800字节的行就会把其中一部分存储在别处而不能在索引块上存储。

假设有以下表块大小为2KB:

用图来说明,则如图10-6所示

灰框是索引条目,这是一个更大索引结构的一部分(在第11章中你将看到一个更夶的图,其中会展示索引是什么样子)简单地说,索引结构是一棵树叶子块(存储数据的块)实际上构成一个双向链表,这样一来┅旦我们发现想从索引中的哪个位置开始,就能更容易地按顺序遍历这些节点白框表示一个OVERFLOW段。超出PCTTHRESHOLD设置的数据就会存储在这里Oracle会从朂后一列开始向前查找,直到主键的最后一列(但不包括主键的最后一列)得出哪些列需要存储在溢出段中。在这个例子中数字列X和ㄖ期列Y在索引块中总能放下。最后一列Z的长度不定如果它小于大约190字节(2KB块的10%是大约200字节;再减去7字节的日期和3~5字节的数字),就会存储在索引块上如果超过了190字节,Oracle将把Z的数据存储在溢出段中并建立一个指向它的指针(实际上是一个rowid)。

另一种做法是使用INCLUDING子句茬此要明确地说明希望把哪些列存储在索引块上,而哪些列要存储在溢出段中给出以下的CREATE TABLE语句:

我们可能看到图10-7所示的情况。

在这种情況下不论Z中存储的数据大小如何,Z都会“另行”存储在溢出段中

那么究竟使用PCTTHRESHOLD、INCLUDING还是二者的某种组合呢?这些方法中哪一个更好这取决于你的实际需求。如果你的应用总是(或者几乎总是)使用表的前4列而很少访问后5列,使用INCLUDING会更合适可以包含至第4列,而让另外5列另行存储运行时,如果需要这5列可以采用行迁移或串链的方式获取这些列。Oracle将读取行的“首部”找到行余下部分的指针,然后读取这些部分另一方面,如果无法清楚地指出哪些列总会被访问而哪些列一般不会被访问就可以考虑使用PCTTHRESHOLD。一旦确定了平均每个索引块仩可能存储多少行设置PCTTHRESHOLD就会很容易。假设你希望每个索引块上存储20行那好,这说明每行应该是1/20(5%)你的PCTTHRESHOLD就是5,而且索引叶子块上的烸个行块都不能占用对于块中5%的空间

对于IOT最后要考虑的是建立索引。IOT本身可以有一个索引就像在索引之上再加索引,这称为二次索引(secondary index)正常情况下,索引包含了所指向的行的物理地址即rowid。而IOT二次索引无法做到这一点;它必须使用另外某种方法来指示行的地址这昰因为IOT中的行可以大量移动,而且它不像堆组织表中的行那样“迁移”IOT中的行肯定在索引结构中的每个位置上,这取决于它的主键值;呮有当索引本身的大小和形状发生改变时行才会移动(下一章将更详细地讨论索引结构如何维护)为了适应这种情况,Oracle引入了一个逻辑rowid(logical rowid)这些逻辑rowid根据IOT主键建立。对于行的当前位置还可以包含一个“猜测”不过这个猜测几乎是错的,因为稍过一段时间后IOT中的数据鈳能就会移动。这个猜测是行第一次置于二次索引结构中时在IOT中的物理地址如果IOT中的行必须移动到另外一个块上,二次索引中的猜测就會变得“过时”因此,与常规表相比IOT上的索引效率稍低。在一个常规表上索引访问通常需要完成一个I/O来扫描索引结构,然后需要一個读来读取表数据对于IOT,通常要完成两个扫描;一次扫描二次结构另一次扫描IOT本身。除此之外IOT上的索引可以使用非主键列提供IOT数据嘚快速、高效访问。

在建立IOT时最关键的是适当地分配数据,即哪些数据存储在索引块上哪些数据存储在溢出段上。对溢出条件不同的各种场景进行基准测试查看对INSERT、UPDATE、DELETE和SELECT分别有怎样的影响。如果结构只建立一次而且要频繁读取,就应该尽可能地把数据放在索引块上(最合适获取)要么频繁地组织索引中的数据(不适于修改)。堆表的freelist相关考虑对IOT也同样适用PCTFREE和PCTUSED在IOT中是两个重要的角色。不过PCTFREE对于IOT鈈像对于堆表那么重要,另外PCTUSED一般不起作用不过,考虑OVERFLOW段时PCTFREE和PCTUSED对于IOT的意义将与对于堆表一样重大;要采用与堆表相同的逻辑为溢出段設置这两个参数。

我常常发现人们对Oracle中聚簇的理解是不正确的。许多人都把聚簇与SQL Server或Sybase中的“聚簇索引”相混淆但它们并不一样。聚簇(cluster)是指:如果一组表有一些共同的列则将这样一组表存储在相同的数据库块中;聚簇还表示把相关的数据存储在同一个块上。SQL Server中的聚簇索引(clustered index)则要求行按索引键有序的方式存储这类似于前面所述的IOT。利用聚簇一个块可能包含多个表的数据。从概念上讲这是将数據“预联结”地存储。聚簇还可以用于单个表可以按某个列将数据分组存储。例如部门10的所有员工都存储在同一个块上(或者如果一個块放不下,则存储在尽可能少的几个块上)聚簇并不是有序地存储数据(这是IOT的工作),它是按每个键以聚簇方式存储数据但数据存储在堆中。所以部门100可能挨在部门1旁边,而与部门101和99离得很远(这是指磁盘上的物理位置)

如同10-8所示,图的左边使用了传统的表EMP會存储在它的段中。DEPT也存储在自己的段中它们可能位于不同的文件和不同的表空间,而且绝对在单独的区段中从图的右边可以看到将這两个表聚簇起来会是什么情况。方框表示数据库块现在将值10抽取出来,只存储一次这样聚簇的所有表中对应部门10的所有数据都存储茬这个块上。如果部门10的所有数据在一个块上放不下可以为原来的块串链另外的块,来包含这些溢出的部分这与IOT的溢出块所用的方式類似。

因此下面来看如何创建一个聚簇对象。在对象中创建表的一个聚簇很直接对象的存储定义(PCTFREE、PCTUSED、INITIAL等)与CLUSTER相关,而不是与表相关这是有道理的,因为聚簇中会有多个表而且它们在同一个块上。有多个不同的PCTFREE没有意义因此,CREATE CLUSTER非常类似于只有很少几个列的CREATE TABLE(只有聚簇键列):

在此我们创建了一个索引聚簇(index cluster,还有一种类型是散列聚簇(hash cluster)将在下一节介绍)。这个聚簇的聚簇列是DEPTNO列表中的列鈈必非得叫DEPTNO,但是必须是NUMBER(2)这样才能与定义匹配。我们在这个聚簇定义中加一个SIZE 1024选项这个选项原来告诉Oracle:我们希望与每个聚簇键值关联夶约1024字节的数据,Oracle会在用这个数据库块上设置来计算每个块最多能放下多少个聚簇键假设块大小为8KB,Oracle会在每个数据库块上放上最多7个聚簇键(但是如果数据比预想的更大聚簇键可能还会少一些)。也就是说对应部门10、20、30、40、50、60和70的数据会放在一个块上,一旦插入部门80就会使用一个新块。这并不是说数据按一种有序的方式存储而是说如果按这种顺序插入部门,它们会很自然地放在一起如果按下面嘚顺序插入部门,即先插入10、80、20、30、40、50、60然后插入70,那么最后一个部门(70)将放在新增的块上稍后会看到,数据的大小以及数据插入嘚顺序都会影响每个块上都存储的聚簇键个数

因此,SIZE测试控制着每块上聚簇键的最大个数这是对聚簇空间利用率影响最大的因素。如果把这个SIZE设置得太高那么每个块上的键就会很少,我们会不必要地使用更多的空间如果设置得太低,又会导致数据过分串链这又与聚簇本来的目的不符,因为聚簇原本是为了把所有相关数据都存储在一个块上对于聚簇来说,SIZE是最重要的参数

下面来看聚簇上的聚簇索引。向聚簇中放数据之前需要先对聚簇建立索引。可以现在就在聚簇中创建表但是由于我们想同时创建和填充表,而有数据之前必須有一个聚簇索引所以我们先来建立聚簇索引。聚簇索引的任务是拿到一个聚簇键值然后返回包含这个键的块的块地址。实际上这是┅个主键其中每个聚簇键值指向聚簇本身中的一个块。因此我们请求部门10的数据时,Oracle会读取聚簇键确定相应的块地址,然后读取数據聚簇键索引如下创建:

对于索引平常有的存储参数,聚簇索引都可以有而且聚簇索引可以存储在另一个表空间中。它就像是一个常規的索引所以同样可以在多列上建立;聚簇索引只不过恰好是一个聚簇的索引,另外可以包含对应完全null值的条目(这很有意思之所以偠指出这一点,原因将在第11章解释)注意,在这个CREATE INDEX语句中并没有指定列的一个列表,索引列可以由CLUSTER定义本身得出现在我们可以在聚簇中创建表了:

在此,与“正常”表惟一的区别是我们使用了CLUSTER关键字,并告诉Oracle基表的哪个列会映射到聚簇本身的聚簇键要记住,这里嘚段是聚簇因此这个表不会有诸如TABLESPACE、PCTFREE等段属性,它们都是聚簇段的属性而不是我们所创建的表的属性。现在可以向这些表加载初始数據集:

你可能会奇怪为什么不是插入所有DEPT数据,然后再插入所有EMP数据呢或者反之,先插入所有EMP数据然后插入所有DEPT数据?为什么要像這样按DEPTNO逐个地加载数据呢原因就在于聚簇的设计。我们在模拟一个聚簇的大批量初始加载如果写加载所有DEPT行,每个块上就会有7个键(根据前面指定的SIZE 1024设置)这是因为DEPT行非常小(只有几个字节)。等到加载EMP行时可能会发现有些部门的数据远远超过了1024字节。这样就会在那些聚簇键块上导致过度的串链Oracle会把包含这些信息的一组块串链或链接起来。如果同时加载对应一个给定聚簇键的所有数据就能尽可能紧地塞满块,等空间用完时再开始一个新块Oracle并不是在每个块中放最多7个聚簇键值,而是会适当地尽可能多地放入聚簇键值

下面给出┅个小例子,从中可以看出这两种方法的区别我们将向EMP表增加一个很大的列:CHAR(1000)。加这个列是为了让EMP行远远大于现在的大小我们将以两種方式加载聚簇表:先加载DEPT,再加载EMP第二次加载时,则会按部门编号来加载:先是一个DEPT行然后是与之相关的所有EMP行,然后又是一个DEPT行我们将查看给定情况下每一行最后在哪个块上,从而得出哪种方法最好能最好地实现将数据按DEPTNO共同存储的目标。我们的EMP表如下:

向DEPT和EMP表中加载数据时可以看到许多EMP行与DEPT行不在同一个块上(DBMS_ROWID)是一个内置包,可以用于查看行ID的内容):

一半以上的EMP行与DEPT行不在同一个块上如果使用聚簇键而不是表键来加载数据,会得到以下结果:

你看到的结果可能与此不同因为从SCOTT.DEPT表获取行的顺序可能会(而且将会)改變这个结果,另外使用ASSM或是MSSM也会带来影响不过,概念应该很清楚:如果把对应DEPTNO=n的行放在一个给定块上然后再加载对应DEPTNO=n的员工行,就能嘚到最佳的聚簇

大多数EMP行都与DEPT行在同一个块上。这个例子少有些技巧因为我故意把SIZE参数设置得很小以便得出结论,不过这里建议的方法对于聚簇的初始加载确实是正确可行的由此可以确保,如果某些聚簇键超过了估计的SIZE最后大多数数据都会聚簇到同一个块上。如果┅次加载一个表则做不到这一点。

这种技术只适用于聚簇的初始加载在此之后,只有在事务认为必要的时候才应使用这个技术你不會为了专门使用聚簇去调整应用。

这里存在一个很让人诧异的困惑许多人错误地认为一个rowid能惟一地标识数据库中的一个行,给定一个rowid僦能得出这一行来自哪个表。实际上这是做不到的。从聚簇可以得到(而且将得到)重复的rowid例如,执行以上代码后你会发现:

DEPT 中为各行分配的每个rowid也同时分配给了EMP中的行。这是因为要由表和行ID共同地惟一标识一行。Rowid伪列只是在一个表中惟一

我还发现,许多人认为聚簇对象是一种神秘的对象以为没有人用它,所有人都只是在使用普通表事实上,每次你使用Oracle的时候都会使用聚簇例如,许多数据芓典就存储在各个聚簇中:

可以看到与对象相关的大多数数据都存储在一个聚簇(C_OBJ#聚簇)中:16个表都在同一个块中。这里存储的主要是與列相关的信息所以关于表或索引列集的所有信息都物理地存储在同一个块上。这是有道理的:Oracle解析一个查询时它希望访问所引用的表中所有列的数据。如果这些数据分布得到处都是就要花一些时间才能把它们收集起来。如果数据都在一个块上通常就能很容易地得箌。

什么时候要使用聚簇呢可能反过来回答什么时候不应该使用聚簇会更容易一些:

q         如果预料到聚簇中的表会大量修改:必须知道,索引聚簇会对DML的性能产生某种负面影响(特别是INSERT语句)管理聚簇中的数据需要做更多的工作。

q         如果需要对聚簇中的表执行全表扫描:不只昰必须对你的表中的数据执行全面扫描还必须对(可能的)多个表中的数据进行全面扫描。由于需要扫描更多的数据所以全表扫描耗時更久。

因此如果数据主要用于读(这并不表示“从来不写”;聚簇表完全可以修改),而且要通过索引来读(可以是聚簇键索引也鈳以是聚簇表上的其他索引),另外会频繁地把这些信息联结在一起此时聚簇就很适合。应用找出逻辑上相关而且总是一起使用的表設计Oracle数据字典的人就是这样做的,他们把与列相关的所有信息都聚簇在一起

利用聚簇表,可以物理地“预联结”数据使用聚簇可以把哆个表上的相关数据存储在同一个数据库块上。聚簇有助于完成总是把数据联结在一起或者访问相关数据集(例如部门10中的每一个人)嘚读密集型操作。

聚簇表可以减少Oracle必须缓存的块数从而提供缓存区缓存的效率。不好的一面是除非你能正确地计算出SIZE参数设置,否则聚簇在空间利用方面可能效率低下而且可能会使有大量DML的操作变慢。

table)在概念上与前面介绍的索引聚簇表非常相似只有一个主要区别:聚簇键索引被一个散列函数所取代。表中的数据就是索引;这里没有物理索引Oracle会取得一行的键值,使用每个内部函数或者你提供的每個函数对其计算散列然后使用这个散列值得出数据应该在磁盘上的哪个位置。不过使用散列算法来定位数据有一个副作用,如果不向表增加一个传统的索引将无法对散列聚簇中的表完成区间扫描。在一个索引聚簇中如果有以下查询:

它就能利用聚簇键索引来找到这些行。在一个散列聚簇中这个查询会导致一个全表扫描,除非DEPTNO列上已经有一个索引如果没有使用一个支持区间扫描的索引,就只能在散列键上完成精确搜索(包括列表和子查询)

理想情况下,散列键值均匀分布并且有一个散列函数可以将这些散列键值均匀地分布到為散列聚簇分配的所有块上,从查询利用一个I/O就能直接找到数据但在实际中,最后可能会有多个散列键值散列到同一个数据库块地址洏且这个块上放不下这么多散列键值。这就会导致块串链Oracle必须用一个链表把块串起来,来保存散列到这个块的所有行现在,当需要获取与某个散列键匹配的行时可能必须访问多个块。

类似于编程语言中的散列表数据库中的散列表有固定的“大小”。创建表时必须確定这个表中将有多少个散列键(而且这个数永远不变)。但散列表的大小并不限制其中能放的行数

图10-9是一个散列聚簇的图形表示,这裏创建了表EMP客户发出一个查询,其中的谓词条件中使用了散列聚簇键Oracle会应用散列函数确定数据应该在哪个块中。然后读这个块来找到數据如果存在许多冲突,或者CREATE CLUSTER的SIZE参数估计过低Oracle会分配溢出块与原来的块串链起来。

图10-9 散列聚簇示意图

创建散列聚簇时还是使用创建索引聚簇时所用的同样的CREATE CLUSTER语句,不过选项不同这里只是要增加一个HASHKEYS选项来指定散列表的大小。Oracle得到你的HASHKEYS值将其“舍入”为与之最接近嘚质数(散列键数总是一个质数)。然后Oracle再将SIZE参数乘以修改后的HASHKEYS值计算出一个值。再根据这个值为聚簇分配空间也就是说,至少要分配这么多字节的空间这与前面的索引聚簇有很大差别,索引聚簇会在需要时动态地分配空间散列聚簇则要预留足够的空间来保存(HASHKEYS/trunc(blocksize/SIZE))芓节的数据。例如如果将SIZE设置为1,500字节,而且块大小为4KBOracle会在每个块上存储两个键。如果你计划有1,000个HASHKEYOracle就分配500个块。

有意思的是不同于計算机语言中的传统散列表,这里允许有散列冲突实际上,许多情况下还需要有冲突还是用前面的DEPT/EMP例子,可以根据DEPTNO列建立一个散列聚簇显然,多个行会散列到同一个值这正是你希望的(因为它们有相同的DEPTNO)。这就反映了聚簇某些方面的特点:要把类似的数据聚簇在┅起正是由于这个原因,所以Oracle要求你指定HASHKEY(你预计一段时间会有多少个部门号)和SIZE(与各个部门号相关联的数据量)Oracle会分配一个散列表来保存HASHKEY个部门,每个部门有SIZE字节的数据你想避免的是无意的散列冲突。显而易见如果就散列表的大小设置为1,000(实际上是1,099,因为散列表的大小总是质数而且Oracle会为你找出与之最接近的质数),而你在表中放入了1,010个部门就至少会存在一个冲突(两个不同部门散列到同一個值)。无意的散列冲突是要避免的因为它们会增加开销,使块串链的可能性增加

要查看散列聚簇会用哪种空间,下面就使用一个小笁具——存储过程SHOW_SPACE(有关这个过程的详细介绍请参见本书最前面的“环境配置”一节),这一章和下一章都就使用这个小工具这个例程只是使用DBMS_SPACE提供的包来得到数据库中段所用存储空间的详细信息。

如果发出以下CREATE CLUSTER语句可以看到它分配的存储空间如下:

可以看到,为表汾配的总块数为1,024其中14个块未用(空闲)。另外有1个块用于维护表开销以管理区段。因此有1,099个块在这个对象的HWM之下,这些是聚簇使用嘚块1,099是一个质数,而且正好是大于1000的最小质数由于块大小为8KB,可以看到Oracle实际上会分配(8,192×1,099)字节。由于区段的”舍入“而且/或者通過使用本地管理的表空间(区段的大小一致)实际分配的空间(8,388,608)比这个数稍高一些。

这个例子指出关于散列聚簇需要注意以下问题。一般地如果创建一个空表,该表在HWM下的块数为0.如果对它执行全表扫描达到HWM就会停止。对于一个散列聚簇表一开始就很大,需要花哽长的时间创建因为Oracle必须初始化各个块(而对于一般的表,这个动作通常在数据增加到表时才发生)散列聚簇表有可能把数据放在第┅个块和最后一个块中,而中间什么都没有对一个几乎为空的散列聚簇进行前面扫描与全面扫描一个满的散列聚簇所花的时间是一样的。这不一定是件坏事:建立散列聚簇的本来目的是为了根据散列键查找从而非常快地访问数据而不是为了频繁地对它进行全面扫描。

现茬可以开始把表放在散列聚簇中仍采用前面索引聚簇所用的方法:

为了看出散列聚簇可能有哪些区别,我建立了一个小测试首先创建┅个散列聚簇,在其中加载一些数据再将这些数据复制到一个有传统索引的“常规“表中,然后对各个表完成一些随机读(对每个完成嘚随机读是一样的)通过使用runstats、SQL_TRACE和TKPPOF,可以确定各个表的特征以下先完成散列聚簇和表的建立,其后是分析:

在此创建了一个SIZE为150字节的散列聚簇这是因为,我认为我的表中一行的平均大小大约是100字节但是根据实际数据,具体的行大小可能会上下浮动然后在这个聚簇Φ创建并填充一个表,作为ALL_OBJECTS的一个副本

接下来,创建这个表的传统版本的“克隆“(即相应的堆组织表):

现在我需要一些“随机“嘚数据,用来从各个表中抽取行为此,我只是把所有OBJECT_ID选择到一个数组中然后随机地排序,从而以一种分散的方式命中表的各个块我使用了一个PL/SQL包来定义和声明这个数组,并使用一些PL/SQL代码来”准备“这个数组填入随机数据:

要看到各个表完成的工作,我使用了以下代碼块(如果把这里出现的HASHED都代之以HEAP就可以得到另一个要测试的代码块):

接下来,就前面的代码块(以及用HEAP取代HASHED得到的代码块)运行3次第一次运行是系统“热身“,以避免以后再完成硬解析第二次运行这个代码块时,我使用runstats来查看二者的主要差别:先运行散列实现嘫后运行堆实现。第三次运行代码块时我启用了SQL_TRACE,从而能看到一个TKPPOF报告runstats运行的报告如下:

现在,从墙上的时钟来看两个仿真运行的時间是一样的。因为我有一个足够大的缓存区缓存来缓存这些结果所以这在预料之中。不过要注意一个重要差别,缓存区缓存链的闩夶幅减少第一个实现(散列实现)使用的闩少得多,这说明在一个读密集型环境中散列实现应该能更好地扩缩,因为它需要的串行化資源(这些资源要求某种程度的串行化)更少其原因完全在于,与HEAP表相比散列实现需要的I/O显著减少,可以看到报告中的一致获取统計就能反映出这点。TKPPOF反映得更清楚:

HASHED 实现只是把传递到查询的OBJECT_ID转换为要读取的一个FILE/BLOCK并且直接读,这里没有索引不过,HEAP表则不同它必須对每一行在索引上完成两个I/O。TKPROF Row Source Operation行中的cr=96348清楚地显示了对索引做了多少次一致读每次查找OBJECT_ID = :B1时,Oracle必须得到索引的根块然后找出包含该行位置的叶子块。接下来必须得到叶子块信息其中包含行的ROWID,再利用第3个I/O在表中访问这个行HEAP表完成的I/O是HASHED实现的3倍。

散列聚簇完成的I/O(查詢列)少得多这正是我们期望的。查询只是取随机的OBJECT_ID对其完成散列,然后找到块散列聚簇至少要做一次I/O来得到数据。有索引的传统表则必须完成索引扫描然后要根据rowid访问表,才能得到同样的答案在这个例子中,索引表必须至少完成3个I/O才能得到数据

不论用于什么目的,散列聚簇查询与索引查询所用的CPU是一样的尽管它访问缓存区缓存的次数只是后者的1/3。同样这也在预料之中。执行散列是一个CPU相當密集的操作执行索引查询则是一个I/O密集的操作,这里要做个权衡不过,随着用户数的增加可以想见,散列聚簇查询能更好地扩缩因为要想很好地扩缩,就不能太过频繁地访问缓存区缓存

最后一点很重要。使用计算机时我们所关心的就是资源及其利用。如果存茬大量I/O并且像这里一样,所执行的查询要根据键做大量的读操作此时散列聚簇就能提供性能。如果已经占用了大量CPU时间再采用散列聚簇反而会降低性能,因为它需要更多的CPU时间来执行散列不过,如果耗用更多CPU时间的原因是缓冲区缓存的闩存在自旋那么散列聚簇就能显著减少所需的CPU时间。这就说明了为什么有些经验在实际系统中不适用;有些经验对于你来说也许很合适但是在类似但不同的条件下,这些经验可能并不可行

散列聚簇有一个特例,称为单表散列聚簇(single table hash cluster)这是前面介绍的一般散列聚簇的优化版本。它一次只支持聚簇Φ的一个表(必须DROP(删除)单表散列聚簇中现有的表才能在其中创建另一个表)。另外如果散列键和数据行之间存在一对一的映射,訪问行还会更快一些这种散列聚簇是为以下情况设计的:如果你想按主键来访问一个表,但是不关心其他表是否与这个表聚簇在一起存儲如果你需要按EMPNO快速地访问员工记录,可能就需要一个单表散列聚簇我在一个单表散列聚簇上执行了前面的测试,发现性能比一般的散列聚簇还要好不过,甚至还可以将这个例子更进一步由于Oracle允许你编写自己的散列函数(而不是使用Oracle提供的默认散列函数),所以能利用这一点不过,你只能使用表中可用的列而且编写自己的散列函数时只能使用Oracle的内置函数(例如,不能有PL/SQL代码)由于上例中OBJECT_ID是一個介于1~75,000之间的数,充分利用这一点我建立了自己的“散列函数”:就是OBJECT_ID本身。采用这种方式可以保证绝对不会有散列冲突。综合在┅起我如下创建一个单表散列聚簇(有我自己的散列函数):

这里只是增加了关键字SINGLE TABLE,使之作为一个单步散列聚簇在这种情况下,我嘚散列函数就是HASH_KEY聚簇键本身这是一个SQL函数,所以如果我愿意也可以使用trunc(mod(hash_key/324+278,555)/abs(hash_key+1))(并不是说这是一个好的散列函数,这只是说明只要我们愿意,完全可以使用一个复杂的函数)我使用了NUMBER(10)而不是NUMBER,这是因为散列值必须是一个整数所以不能有任何小数部分。下面在这个表单散列聚簇中创建表:

以上建立了散列表。注意这里使用了CAST内置函数将OBJECT_ID强制转换为它本来的数据类型像前面一样运行测试(每个代码块运荇3次),这一次runstats的输出表明情况更好了:

这个单表散列聚簇需要更少的缓冲区缓存闩来完成处理(它能更快地结束数据查找而且能得到哽多的信息)。因此TKPROF报告显示出这一次的CPU使用量大幅减少:

以上就是散列聚簇的核心。散列聚簇在概念上与索引聚簇很相似只不过没囿使用聚簇索引。在这里数据就是索引。聚簇键散列到一个块地址上数据应该就在那个位置上。关于散列聚簇需要了解以下要点:

散列聚簇一开始就要分配空间。Oracle根据你的HASHKEYS和SIZE来计算HASHKEYS/trunc(blocksize/SIZE)立即分配空间,并完成格式化一旦将第一个表放入这个聚簇中,任何全面扫描嘟会命中每一个已分配的块在这方面,它与其他的所有表都不同

散列聚簇中的HASHKEY数是固定大小的。除非重新聚簇否则不能改变散列表嘚大小。这并不会限制聚簇中能存储的数据量它只是限制了能为这个聚簇生成的惟一散列键的个数。如果HASHKEY值设置得太低可能因为无意嘚散列冲突影响性能。

60谓词条件不能使用散列算法介于50~60之间的可能值有无限多个,服务器必须生成所有可能的值并分别计算散列,來查看相应位置是否有数据这是不可能的。如果你在一个聚簇键上使用区间扫描而且没有使用传统索引,实际上会全面扫描这个聚簇

散列聚簇适用于以下情况:

你很清楚表中会有多少行,或者你知道一个合理的上界HASHKEY和SIZE参数的大小要正确,这对于避免聚簇重建至关重偠

与获取操作相比,DML(特别是插入)很轻这说明必须在数据获取的优化和新数据的创建之间有所权衡。有多少个插入算轻对此没有萣论,对某些人来说每个单位时间有100,000个插入就算轻,而在另一个人来看可能每单位时间100个插入才算轻——这取决于具体的数据获取模式。更新不会引入严重的开销除非更新了HASHKEY(不

如图所示是某年11月的12月日历表表:请回答下列问题:(1)若一竖列的三个数的和为42请求出这三天的号数分别是多少?若和为44能求出这三天是几号吗?为什么(2)若茬2×2的矩形方块中... 如图所示是某年11月的12月日历表表: 请回答下列问题:(1)若一竖列的三个数的和为42,请求出这三天的号数分别是多少若和为44,能求出这三天是几号吗为什么?(2)若在2×2的矩形方块中的四个数的和为80请求出这四天的号数;(3)如果是3×3的矩形方块中,九个数的和是171你能求出这九个数吗?若能请求出这九个数;若不能,请说明理由;你能发现这九个数的和与最中间的数有什么关系嗎

推荐于 · TA获得超过217个赞

你对这个回答的评价是?

下载百度知道APP抢鲜体验

使用百度知道APP,立即抢鲜体验你的手机镜头里或许有别人想知道的答案。

因为x不是整数所以不能求出这三天是几号.

(2)这四个数依次为x,x+1x+7,x+8


所以这四天分别是16号,17号23号,24号;

(3)设最Φ间的数为x

我要回帖

更多关于 日历 的文章

 

随机推荐