Oracle 9i & 10g编程艺术-深入数据库体系结构——第11章:索引

来源:互联网 发布:进销存软件免费版下载 编辑:程序博客网 时间:2024/05/18 00:39

第11章                      索引

索引是应用设计和开发的一个重要方面。如果有太多的索引,DML的性能就会受到影响。如果索引太少,又会影响查询(包括插入、更新和删除)的性能。要找到一个合适的平衡点,这对于应用的性能至关重要。

我常常发现,人们在应用开发中总是事后才想起索引。我坚持认为这是一种错误的做法。如果你知道数据将如何使用,从一开始就应该能提出应用中要使用怎样的索引,即具有一组代表性的索引。不过,一般的做法却往往是随应用“放任自流“,过后才发现哪里需要索引,这种情况实在太多了。这说明,你没有花时间来了解数据将如何使用以及最终要处理多少行。经过一段时间后,随着数据量的增长,你会不停地向系统增加索引(也就是说,你所执行的是一种反应式调优)。你就有一些冗余而且从不使用的索引,这不仅会浪费空间,还会浪费计算资源。磨刀不误砍柴工,如果刚开始的时候花几个小时好好地考虑何时为数据加索引,以及如何加索引,这肯定能在以后的”调优“中节省更多的时间(注意,我所说的是”肯定能“节省更多时间,而不只是”可能“节省更多时间)。

这一章的主旨是对Oracle中可用的索引提供一个概述,讨论什么时候以及在哪里可以使用索引。这一章的风格和格式与本书其他章有所不同。索引是一个很宽泛的主题,光是介绍索引就可以单独写一本书,其部分原因是:索引是开发人员和DBA角色之间的一个桥梁。一方面,开发人员必须了解索引,清楚如何在应用中使用索引,而且知道何时使用索引(以及何时不使用索引)等。另一方面,DBA则要考虑索引的增长、索引中存储空间的使用以及其他物理特性。我们将主要从应用角度来考虑,也就是从索引的实际使用来介绍索引。这一章前半部分提供了一些基本知识。这一章的后半部分回答了关于索引的一些最常问到的问题。

这一章中的各个例子分别需要不同的Oracle版本中的特性。如果每个例子需要Oracle企业版或个人版的某些特性(而标准版中不支持),我会明确地指出来。

11.1   Oracle索引概述

Oracle提供了多种不同类型的索引以供使用。简单地说,Oracle中包括如下索引:

B*树索引:这些是我所说的“传统“索引。到目前为止,这是Oracle和大多数其他数据库中最常用的索引。B*树的构造类似于二叉树,能根据键提供一行或一个行集的快速访问,通常只需很少的读操作就能找到正确的行。不过,需要注意重要的一点,”B*树“中的”B“不代表二叉(binary),而代表平衡(balanced)。B*树索引并不是一颗二叉树,这一点在介绍如何在磁盘上物理地存储B*树时就会了解到。B*树索引有以下子类型:

索引组织表(index organized table):索引组织表以B*树结构存储。堆表的数据行是以一种无组织的方式存储的(只要有可用的空间,就可以放数据),而IOT与之不同,IOT中的数据要按主键的顺序存储和排序。对应用来说,IOT表现得与“常规“表并无二致;需要使用SQL来正确地访问IOTIOT对信息获取、空间系统和OLAP应用最为有用。IOT在上一章已经详细地讨论过。

B*树聚簇索引(B*tree cluster index)这些是传统B*树索引的一个变体(只是稍有变化)。B*树聚簇索引用于对聚簇键建立索引(见第11.章中“索引聚簇表“一节),所以这一章不再讨论。在传统B*树中,键都指向一行;而B*树聚簇不同,一个聚簇键会指向一个块,其中包含与这个聚簇键相关的多行。

降序索引(descending index):降序索引允许数据在索引结构中按“从大到小“的顺序(降序)排序,而不是按”从小到大“的顺序(升序)排序。我们会解释为什么降序索引很重要,并说明降序索引如何工作。

反向键索引(reverse key index):这也是B*树索引,只不过键中的字节会“反转“。利用反向键索引,如果索引中填充的是递增的值,索引条目在索引中可以得到更均匀的分布。例如,如果使用一个序列来生成主键,这个序列将生成诸如987500987501987502等值。这些值是顺序的,所以倘若使用一个传统的B*树索引,这些值就可能放在同一个右侧块上,这就加剧了对这一块的竞争。利用反向键,Oracle则会逻辑地对205789105789005789等建立索引。Oracle将数据放在索引中之前,将先把所存储数据的字节反转,这样原来可能在索引中相邻放置的值在字节反转之后就会相距很远。通过反转字节,对索引的插入就会分布到多个块上。

位图索引(bitmap index):在一颗B*树中,通常索引条目和行之间存在一种一对一的关系:一个索引条目就指向一行。而对于位图索引,一个索引条目则使用一个位图同时指向多行。位图索引适用于高度重复而且通常只读的数据(高度重复是指相对于表中的总行数,数据只有很少的几个不同值)。考虑在一个有100万行的表中,每个列只有3个可取值:YNNULL。举例来说,如果你需要频繁地统计多少行有值Y,这就很适合建立位图索引。不过并不是说如果这个表中某一列有11.000个不同的值就不能建立位图索引,这一列当然也可以建立位图索引。在一个OLTP数据库中,由于存在并发性相关的问题,所以不能考虑使用位图索引(后面我们就会讨论这一点)。注意,位图索引要求使用Oracle企业版或个人版。

位图联结索引(bitmap join index):这为索引结构(而不是表)中的数据提供了一种逆规范化的方法。例如,请考虑简单的EMPDEPT表。有人可能会问这样一个问题:“多少人在位于波士顿的部门工作?“EMP有一个指向DEPT的外键,要想统计LOC值为Boston的部门中的员工人数,通常必须完成表联结,将LOC列联结至EMP记录来回答这个问题。通过使用位图联结索引,则可以在EMP表上对LOC列建立索引。

基于函数的索引(function-based index):这些就是B*树索引或位图索引,它将一个函数计算得到的结果存储在行的列中,而不是存储列数据本身。可以把基于函数的索引看作一个虚拟列(或派生列)上的索引,换句话说,这个列并不物理存储在表中。基于函数的索引可以用于加快形如SELECT * FROM T WHERE FUNCTION(DATABASE_COLUMN) = SAME_VALUE这样的查询,因为值FUNCTION(DATABASE_COLUMN)已经提前计算并存储在索引中。

应用域索引(application domain index):应用域索引是你自己构建和存储的索引,可能存储在Oracle中,也可能在Oracle之外。你要告诉优化器索引的选择性如何,以及执行的开销有多大,优化器则会根据你提供的信息来决定是否使用你的索引。Oracle文本索引就是应用域索引的一个例子;你也可以使用构建Oracle文本索引所用的工具来建立自己的索引。需要指出,这里创建的“索引“不需要使用传统的索引结构。例如,Oracle文本索引就使用了一组表来实现其索引概念。

可以看到,可供选择的索引类型有很多。在下面几节中,我将提供有关的一些技术细节来说明某种索引如何工作,以及应该何时使用这些索引。需要重申一遍,在此不会涵盖某些与DBA相关的主题。例如,我们不打算讨论在线重建索引的原理;而会把重点放在与应用相关的实用细节上。

11.2   B*树索引

B*树索引就是我所说的“传统“索引,这是数据库中最常用的一类索引结构。其实现与二叉查找树很相似。其目标是尽可能减少Oracle查找数据所花费的时间。不严格地说,如果在一个数字列上有一个索引,那么从概念上讲这个结构可能如图11.-1所示。

注意      也许会有一些块级优化和数据压缩,这些可能会使实际的块结构与图11.-1所示并不同。

11.-1    典型的B*树索引布局

这个树最底层的块称为叶子节点(leaf node)或叶子块(leaf block),其中分别包含各个索引键以及一个rowid(指向所索引的行)。叶子节点之上的内部块称为分支块(branch block)。这些节点用于在结构中实现导航。例如,如果想在索引中找到值42,要从树顶开始,找到左分支。我们要检查这个块,并发现需要找到范围在“42..50“的块。这个块将是叶子块,其中会指示包含数42的行。有意思的是,索引的叶子节点实际上构成了一个双向链表。一旦发现要从叶子节点中的哪里”开始“(也就是说,一旦发现第一个值),执行值的有序扫描(也称为索引区间扫描(index range scan))就会很容易。我们不用再在索引结构中导航;而只需根据需要通过叶子节点向前或向后扫描就可以了。所以要满足诸如以下的谓词条件将相当简单:

where x between 20 and 30

Oracle发现第一个最小键值大于或等于20的索引叶子块,然后水平地遍历叶子节点链表,直到最后命中一个大于30的值。

B*树索引中不存在非惟一(nonunique)条目。在一个非惟一索引中,Oracle会把rowid作为一个额外的列(有一个长度字节)追加到键上,使得键惟一。例如,如果有一个CREATE INDEX I ON T(X,Y)索引,从概念上讲,它就是CREATE UNIQUE INDEX I ON T(X,Y,ROWID)。在一个惟一索引中,根据你定义的惟一性,Oracle不会再向索引键增加rowid。在非惟一索引中,你会发现,数据会首先按索引键值排序(依索引键的顺序)。然后按rowid升序排序。而在惟一索引中,数据只按索引键排序。

B*树的特点之一是:所有叶子块都应该在树的同一层上。这一层也称为索引的高度(height),这说明所有从索引的根块到叶子块的遍历都会访问同样数目的块。也就是说,对于形如”SELECT INDEXED_COL FROM T WHERE INDEXED_COL = :X“的索引,要到达叶子块来获取第一行,不论使用的:X值是什么,都会执行同样数目的I/O。换句话说,索引是高度平衡的(height balanced)。大多数B*树索引的高度都是2或者3,即使索引中有数百万行记录也是如此。这说明,一般来讲,在索引中找到一个键只需要执行23I/O,这倒不坏。

­­­注意      Oracle在表示从索引根块到叶子块遍历所涉及的块数时用了两个含义稍有不同的术语。第一个是高度(HEIGHT),这是指从根块到叶子块遍历所需的块数。使用ANALYZE INDEX <name> VALIDATE STRUCTURE命令分析索引后,可以从INDEX_STATS视图找到这个高度(HEIGHT)值。另一个术语是BLEVEL,这是指分支层数,与HEIGHT相差1BLEVEL不把叶子块层算在内)。收集统计信息后,可以在诸如USER_INDEXES之类的常规字典表中找到BLEVEL值。

例如,假设有一个11.,000,000行的表,其主键索引建立在一个数字列上:

big_table@ORA9IR2> select index_name, blevel, num_rows

2 from user_indexes

3 where table_name = 'BIG_TABLE';

INDEX_NAME                   BLEVEL       NUM_ROWS

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

BIG_TABLE_PK                             2            10441513

BLEVEL2,这说明HEIGHT3,要找到叶子需要两个I/O(访问叶子本身还需要第三个I/O)。所以,要从这个索引中获取任何给定的键值,共需要3I/O

big_table@ORA9IR2> select id from big_table where id = 42;

Execution Plan

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

0      SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=11.Bytes=6)

11.0            INDEX (UNIQUE SCAN) OF 'BIG_TABLE_PK' (UNIQUE) (Cost=2 Card=11.Bytes=6)

Statistics

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

...

3 consistent gets

...

11.rows processed

big_table@ORA9IR2> select id from big_table where id = 12345;

Statistics

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

... 3 consistent gets

... 11.rows processed

big_table@ORA9IR2> select id from big_table where id = 1234567;

Statistics

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

...

3 consistent gets

...

11.rows processed

B*树是一个绝佳的通用索引机制,无论是大表还是小表都很适用,随着底层表大小的增长,获取数据的性能只会稍有恶化(或者根本不会恶化)。

11.2.1             索引键压缩

对于B*树索引,可以做的一件有意思的工作是“压缩”。这与压缩ZIP文件的方式不同,它是指从串联(多列)索引去除冗余。

在第11.章的“索引组织表”一节中我们曾经详细地讨论压缩键索引,这里再简要地做个说明。压缩键索引(compressed key index)的基本概念是,每个键条目分解为两个部分:“前缀”和“后缀”。前缀建立在串联索引(concatenated index)的前几列上,这些列有许多重复的值。后缀则在索引键的后几列上,这是前缀所在索引条目中的惟一部分(即有区别的部分)。

下面通过一个例子来说明,我们将创建一个表和一个串联索引,并使用ANALYZE INDEX测量无压缩时所用的空间。然后利用索引压缩创建这个索引,分别压缩不同数目的键条目,查看有什么差别。下面先来看这个表和索引:

ops$tkyte@ORA10G> create table t

2 as

3 select * from all_objects;

Table created.

 

ops$tkyte@ORA10G> create index t_idx on

2 t(owner,object_type,object_name);

Index created.

 

ops$tkyte@ORA10G> analyze index t_idx validate structure;

Index analyzed.

然后创建一个INX_STATS表,在这里存放INDEX_STATS信息,我们把表中的行标记为“未压缩”(noncompressed):

ops$tkyte@ORA10G> create table idx_stats

2 as

3 select 'noncompressed' what, a.*

4 from index_stats a;

Table created.

现在可以看到,OWNER部分重复了多次,这说明,这个索引中的一个索引块可能有数十个条目,如图11.-2所示。

11.-2    有重复OWNER列的索引块

可以从中抽取出重复的OWNER列,这会得到如同11.-3所示的块。

11.-3    抽取了OWNER列的索引块

在图11.-3中,所有者(owner)名在叶子块上只出现了一次,而不是在每个重复的条目上都出现一次。运行以上脚本,传入数字1作为参数来重新创建这个索引,在此索引使用了第一列的压缩:

drop index t_idx;

create index t_idx on

t(owner,object_type,object_name)

compress &1;

analyze index t_idx validate structure;

insert into idx_stats

select 'compress &1', a.*

from index_stats a;

为了进行比较,我们不仅在压缩一列的基础上运行了这个脚本,还分别使用了两个和3个压缩列来运行这个脚本,查看会发生什么情况。最终,我们将查询IDX_STATS,应该能观察到以下信息:

ops$tkyte@ORA10G> select what, height, lf_blks, br_blks,

2 btree_space, opt_cmpr_count, opt_cmpr_pctsave

3 from idx_stats

4 /

WHAT               HEIGHT LF_BLKS   BR_BLKS BTREE_SPACE   OPT_CMPR_COUNT OPT_CMPR_PCTSAVE

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

noncompressed     3        337                 3           2718736                                 2                          28

compress 1             3        300                 3           2421684                                 2                          11.

compress 2             2        240                 1           1926108                                 2                             0

compress 3             3        375                 3           3021084                                 2                          35

可以看到,COMPRESS 1索引的大小大约是无压缩索引的89%(通过比较BTREE_SPACE得出)。叶子块数大幅度下降。更进一步,使用COMPRESS 2时,节省的幅度更为显著。所得到的索引大约是原索引(无压缩索引)的70%,而且由于数据量减少,这些数据能放在单个块上,相应地索引的高度就从3降为2.实际上,利用列OPT_CMPR_PCTSAVE的信息(这代表最优的节省压缩百分比(optimum compression percent saved)或期望从压缩得到的节省幅度)。我们可以猜测出COMPRESS 2索引的大小:

ops$tkyte@ORA10G> select 2718736*(11.0.28) from dual;

 

   2718736*(11.0.28)

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

             1957489.92

注意      对无压缩索引执行ANALYZE命令时,会填写OPT_CMPR_PCTSAVE/OPT_CMPR_COUNT列,并估计出:利用COMPRESS 2,可以节省28%的空间;而事实确实如此,我们果真节省了大约这么多的空间。

不过,再看看COMPRESS 3会怎么样。如果压缩3列,所得到的索引实际上会更大:是原来索引大小的110%。这是因为:每删除一个重复的前缀,能节省N个副本的空间,但是作为压缩机制的一部分,这会在叶子块上增加4字节的开销。把OBJECT_NAME列增加到压缩键后,则使得这个键是惟一的;在这种情况下,则说明没有重复的副本可以提取。因此,最后的结果就是:我们只是向每个索引键条目增加了4个字节,而不能提取出任何重复的数据。IDX_STATS中的OPT_CMPR_COUNT列真是精准无比,确实给出了可用的最佳压缩数,OPT_COMPR_PCTSAVE则指出了可以得到多大的节省幅度。

对现在来说,这种压缩不是免费的。现在压缩索引比原来更复杂了。Oracle会花更多的时间来处理这个索引结构中的数据,不光在修改期间维护索引更耗时,查询期间搜索索引也更花时间。利用压缩,块缓冲区缓存比以前能存放更多的索引条目,缓冲命中率可能会上升,物理I/O应该下降,但是要多占用一些CPU时间来处理索引,还会增加块竞争的可能性。在讨论散列聚簇时,我们曾经说过,对于散列聚簇,获取100万个随机的行可能占用更多的CPU时间,但是I/O数会减半;这里也是一样,我们必须清楚存在的这种折中。如果你现在已经在大量占用CPU时间,在增加压缩键索引只能适得其反,这会减慢处理的速度。另一方面,如果目前的I/O操作很多,使用压缩键索引就能加快处理速度。

11.2.2             反向键索引

B*树索引的另一个特点是能够将索引键“反转”。首先,你可以问问自己“为什么想这么做?” B*树索引是为特定的环境、特定的问题而设计的。实现B*树索引的目的是为了减少“右侧”索引中对索引叶子块的竞争,比如在一个Oracle RAC环境中,某些列用一个序列值或时间戳填充,这些列上建立的索引就属于“右侧”(right-hand-side)索引。

注意      我们在第2章讨论过RAC

RAC是一种Oracle配置,其中多个实例可以装载和打开同一个数据库。如果两个实例需要同时修改同一个数据块,它们会通过一个硬件互连(interconnect)来回传递这个块来实现共享,互连是两个(或多个)机器之间的一条专用网络连接。如果某个利用一个序列填充,这个列上有一个主键索引(这是一种非常流行的实现),那么每个人插入新值时,都会视图修改目前索引结构右侧的左块(见图11.-1,其中显示出索引中较高的值都放在右侧,而较低的值放在左侧)。如果对用序列填充的列上的索引进行修改,就会聚集在很少的一组叶子块上。倘若将索引的键反转,对索引进行插入时,就能在索引中的所有叶子键上分布开(不过这往往会使索引不能得到充分地填充)。

注意      你可能还会注意到,反向键可以用作一种减少竞争的方法(即使只有一个Oracle实例)。不过重申一遍,如这一节所述,反向键主要用于缓解忙索引右侧的缓冲区忙等待。

在介绍如何度量反向键索引的影响之前,我们先来讨论物理上反向键索引会做什么。反向键索引只是将索引键中各个列的字节反转。如果考虑901019010290103这样几个数,使用Oracle DUMP函数查看其内部表示,可以看到这几个数的表示如下:

ops$tkyte@ORA10GR1> select 90101, dump(90101,11.) from dual

2 union all

3 select 90102, dump(90102,11.) from dual

4 union all

5 select 90103, dump(90103,11.) from dual

6 /

 

          90101     DUMP(90101,11.)

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

          90101     Typ=2 Len=4: c3,a,2,2

          90102     Typ=2 Len=4: c3,a,2,3

          90103     Typ=2 Len=4: c3,a,2,4

每个数的长度都是4字节,它们只是最后一个字节有所不同。这些数最后可能在一个索引结构中向右依次放置。不过,如果反转这些数的字节,Oracle就会插入以下值:

ops$tkyte@ORA10GR1> select 90101, dump(reverse(90101),11.) from dual

2 union all

3 select 90102, dump(reverse(90102),11.) from dual

4 union all

5 select 90103, dump(reverse(90103),11.) from dual

6 /

 

          90101     DUMP(REVERSE(90101),1

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

          90101     Typ=2 Len=4: 2,2,a,c3

          90102     Typ=2 Len=4: 3,2,a,c3

          90103     Typ=2 Len=4: 4,2,a,c3

注意      REVERSE函数没有相关的文档说明,因此,使用是当心。我不建议在“实际“代码中使用REVERSE,因为它没有相关的文档,这说明这个函数未得到公开支持。

这些数彼此之间最后会“相距很远“。这样访问同一个块(最右边的块)的RAC实例个数就能减少,相应地,在RAC实例之间传输的块数也会减少。反向键索引的缺点之一是:能用常规索引的地方不一定能用反向键索引。例如,在回答以下谓词时,X上的反向键索引就没有:

where x > 5

存储之前,数据不是按X在索引中排序,而是按REVERSE(X)排序,因此,对X>5的区间扫描不能使用这个索引。另一方面,有些区间扫描确实可以在反向键索引上完成。如果在(X,Y)上有一个串联索引,以下谓词就能够利用反向键索引,并对它执行“区间扫描“:

where x = 5

这是因为,首先将X的字节反转,然后再将Y的字节反转。Oracle并不是将(X||Y)的字节反转,而是会存储(REVERSE(X) || REVERSE(Y))。这说明, X = 5的所有值会存储在一起,所以Oracle可以对这个索引执行区间扫描来找到所有这些数据。

下面,假设在一个用序列填充的表上有一个代理主键(surrogate primary key),而且不需要在这个(主键)索引上使用区间扫描,也就是说,不需要做MAX(primary_key)MIN(primary_key)WHERE primary_key < 100等查询,在有大量插入操作的情况下,即使只有一个Oracle实例,也可以考虑使用反向键索引。我建立了两个不同的测试,一个是在纯PL/SQL环境中进行测试,另一个使用了Pro*C,我想通过这两个测试来展示反向键索引和传统索引对插入的不同影响,即如果一个表的主键上有一个反向键索引,与有一个传统索引的情况相比,完成插入时会有什么差别。在这两种情况下,所用的表都是用以下DDL创建的(这里使用了ASSM来避免表块的竞争,这样可以把索引块的竞争隔离开):

create table t tablespace assm

as

select 0 id, a.*

from all_objects a

where 11.0;

 

alter table t

add constraint t_pk

primary key (id)

using index (create index t_pk on t(id) &indexType tablespace assm);

 

create sequence s cache 1000;

在此如果把&indexType替换为关键字REVERSE,就会创建一个反向键索引,如果不加&indexType(即替换为“什么也没有“),则表示使用一个”常规“索引。要运行的PL/SQL代码如下,将分别由12511.11.个用户并发运行这个代码:

create or replace procedure do_sql

as

begin

for x in ( select rownum r, all_objects.* from all_objects )

loop

              insert into t

                     ( id, OWNER, OBJECT_NAME, SUBOBJECT_NAME,

                     OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, CREATED,

                     LAST_DDL_TIME, TIMESTAMP, STATUS, TEMPORARY,

                     GENERATED, SECONDARY )

              values

                     ( s.nextval, x.OWNER, x.OBJECT_NAME, x.SUBOBJECT_NAME,

                     x.OBJECT_ID, x.DATA_OBJECT_ID, x.OBJECT_TYPE, x.CREATED,

                     x.LAST_DDL_TIME, x.TIMESTAMP, x.STATUS, x.TEMPORARY,

                     x.GENERATED, x.SECONDARY );

              if ( mod(x.r,100) = 0 )

              then

                     commit;

              end if;

end loop;

commit;

end;

/

我们已经在第9章讨论过PL/SQL提交时优化,所以现在我想运行使用另一种环境的测试,以免被这种提交时优化所误导。我使用了Pro*C来模拟一个数据仓库抽取、转换和加载(extract, transform, load, ETL)例程,它会在提交之间一次成批地处理100行(即每次提交前都处理100行):

exec sql declare c cursor for select * from all_objects;

exec sql open c;

exec sql whenever notfound do break;

for(;;)

{

              exec sql

              fetch c into :owner:owner_i,

              :object_name:object_name_i, :subobject_name:subobject_name_i,

              :object_id:object_id_i, :data_object_id:data_object_id_i,

              :object_type:object_type_i, :created:created_i,

              :last_ddl_time:last_ddl_time_i, :timestamp:timestamp_i,

              :status:status_i, :temporary:temporary_i,

              :generated:generated_i, :secondary:secondary_i;

 

              exec sql

              insert into t

                     ( id, OWNER, OBJECT_NAME, SUBOBJECT_NAME,

                     OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, CREATED,

                     LAST_DDL_TIME, TIMESTAMP, STATUS, TEMPORARY,

                     GENERATED, SECONDARY )

              values

                     ( s.nextval, :owner:owner_i, :object_name:object_name_i,

                     :subobject_name:subobject_name_i, :object_id:object_id_i,

                     :data_object_id:data_object_id_i, :object_type:object_type_i,

                     :created:created_i, :last_ddl_time:last_ddl_time_i,

                     :timestamp:timestamp_i, :status:status_i,

                     :temporary:temporary_i, :generated:generated_i,

                     :secondary:secondary_i );

              if ( (++cnt%100) == 0 )

              {

                     exec sql commit;

              }

}

exec sql whenever notfound continue;

exec sql commit;

exec sql close c;

Pro*C预编译时PREFETCH设置为100,使得这个C代码与上述PL/SQL代码(要求Oracle版本为Oracle 10g)是相当的,它们有同样的表现。

注意      Oracle 10g Release 1及以上版本中,PL/SQL中简单的FOR X IN(SELECT * FROM T)会悄悄地一次批量获取100行,而在Oracle9i及以前版本中,只会一次获取一行。因此,如果想在Oracle9i及以前版本中执行这个测试,就需要修改PL/SQL代码,利用BULK COLLECT语法成批地进行获取。

两种代码都会一次获取100行,然后将数据逐行插入到另一个表中。下表总结了各次运行之间的差别,先从单用户测试开始,如表11.-1所示。

11.-1 利用PL/SQLPro*C对使用反向键索引进行性能测试:单用户

                                 反向                 无反向                 反向              无反向

                               PL/SQL            PL/SQL              Pro*C            Pro*C

事务/                               38.24                      43.45                      11..35                  11..08

CPU时间(秒)                    25                           22                           33                       31

缓冲区忙等待数/时间           0/0                          0/0                          0/0                      0/0

耗用时间(分钟)               0.42                        0.37                        0.92                    0.83

日志文件同步数/时间                                          6/0                      11.940/7               11.940/7

从第一个单用户测试可以看到,PL/SQL代码在执行这个操作时比Pro*C代码高效得多,随着用户负载的增加,我们还将继续看到这种趋势。Pro*C之所以不能像PL/SQL那样很好地扩展,部分原因在于Pro*C必须等待日志文件同步等待,而PL/SQL提供了一种优化可以避免这种日志文件同步等待。

从这个单用户测试来看,似乎方向键索引会占用更多的CPU时间。这是有道理的,因为数据库必须执行额外的工作来反转键中的字节。不过,随着用户数的增加,我们会看到这种情况不再成立。随着竞争的引入,方向键索引的开销会完全消失。实际上,甚至在执行两个用户的测试时,这种开销就几乎被索引右侧的竞争所抵消,如表11.-2所示。

11.-2 利用PL/SQLPro*C对使用反向键索引进行性能测试:两个用户

                                 反向                 无反向                 反向              无反向

                               PL/SQL            PL/SQL              Pro*C            Pro*C

事务/                               46.59                      49.03                      20.07                  20.29

CPU时间(秒)                    77                           73                          104                     101

缓冲区忙等待数/时间       4,267/2                 133,644/2                 3,286/0              23,688/1

耗用时间(分钟)               0.68                        0.65                        11.58                    11.57

日志文件同步数/时间          11./0                        11./0                    3,273/29             2,132/29

从这个两用户的测试中可以看到,PL/SQL还是要优于Pro*C。另外在PL/SQL这一边,使用方向键索引已经开始显示出某种好处,而在Pro*C一边还没有明显的反映。这种趋势也会延续下去。方向键索引可以解决由于索引结构中对最右边的块的竞争而导致的缓冲区忙等待问题,不过,对于影响Pro*C程序的日志文件同步等待问题则无计可施。这正是我们同时执行一个PL/SQL测试和一个Pro*C测试的主要原因:我们就是想看看这两种环境之间有什么差别。由此产生了一个问题:在这种情况下,为什么方向键索引对PL/SQL明显有好处,而对Pro*C似乎没有作用?归根结底就是因为日志文件同步等待事件。PL/SQL能不断地插入,而很少在提交时等待日志文件同步等待事件,而Pro*C不同,它必须每100行等待一次。因此,在这种情况下,与Pro*C相比,PL/SQL更多地要受缓冲区忙等待的影响。在PL/SQL中如果能缓解缓冲区忙等待,它就能处理更多事务,所以方向键索引对PL/SQL很有好处。但是对于Pro*C,缓冲区忙等待并不是根本问题,这不是主要的性能瓶颈,所以消除缓冲区忙等待对于总体性能来说没有说明影响。

下面来看5个用户的测试,如表11.-3所示

11.-3 利用PL/SQLPro*C对使用反向键索引进行性能测试:5个用户

                                 反向                 无反向                 反向              无反向

                               PL/SQL            PL/SQL              Pro*C            Pro*C

事务/                               43.84                      39.78                      11..22                  11..11.

CPU时间(秒)                   389                         395                         561                     588

缓冲区忙等待数/时间     11.,259/45             221,353/153              11.,118/9           157,967/56

耗用时间(分钟)               11.82                        2.00                        4.11.                    4.38

日志文件同步数/时间                                       691/11.                  6,655/73             5,391/82

这里的结果似曾相识。PL/SQL程序运行时几乎没有日志同步等待,所以会显著地受缓冲区忙等待的影响。倘若采用一个传统索引,如果5个用户都试图插入索引结构的右侧,PL/SQL受到缓冲区忙等待的影响最大,相应地,如果能减少这种缓冲区忙等待,所得到的好处也最明显。

下面来看11.个用户测试,如表11.-4所示,可以看到这种趋势还在延续。

11.-4 利用PL/SQLPro*C对使用反向键索引进行性能测试:11.个用户

                                 反向                 无反向                 反向              无反向

                               PL/SQL            PL/SQL              Pro*C            Pro*C

事务/                               45.90                      35.38                      11..88                  11..05

CPU时间(秒)                   781                         789                        11.256                  11.384

缓冲区忙等待数/时间    26,846/279          456,231/11.382          25,871/134      364,556/11.702

耗用时间(分钟)               3.47                        4.50                        8.90                    9.92

日志文件同步数/时间                                      2,602/72               11.,032/196         11.,653/141

PL/SQL程序中完全没有日志文件同步等待,通过消除缓冲区忙等待事件,会大为受益。尽管Pro*C程序出现遭遇到更多的缓冲区忙等待竞争,但是由于它还在频繁地等待日志文件同步事件,所以方向键索引对它的好处不大。对于一个有常规索引的PL/SQL实现,要改善它的性能,一种方法是引入一个小等待。这会减少对索引右侧的竞争,并提高总体性能。由于篇幅有限,这里不再给出11.个和20个用户测试的情况,但是可以确保一点,这一节观察到的趋势还会延续。

在这个演示中,我们得出了两个结论。方向键索引有助于缓解缓冲区忙等待问题:但是取决于其他的一些因素,你的投资可能会得到不同的回报。查看11.用户测试的表11.-4时,可以看到,通过消除缓冲区忙等待(在这里,这是等待最多的等待事件),将对事务吞吐量稍有影响;同时也确实显示出:随着并发程度的提高,可扩展性会增加。而对PL/SQL做同样的工作时,对性能的影响则有很大不同:通过消除这个瓶颈,吞吐量会有大幅提升。

11.2.3             降序索引

降序索引(descending index)是Oracle8i开始引入的,用以扩展B*树索引的功能。它允许在索引中以降序(从大到小的顺序)存储一列,而不是升序(从小到大)存储。在之前的Oracle版本(即Oracle8i以前的版本)中,尽管语法上也支持DESC(降序)关键字,但是一般都会将其忽略,这个关键字对于索引中数据如何存储或使用没有任何影响。不过,在Oracle8i及以上版本中,DESC关键字确实会改变创建和使用索引的方式。

Oracle能往前读索引,这种能力已不算新,所以你可能会奇怪我们为什么会兴师动众地说这个特性很重要。例如,如果使用先前的表T,并如下查询这个表:

ops$tkyte@ORA10G> set autotrace traceonly explain

ops$tkyte@ORA10G> select owner, object_type

2 from t

3 where owner between 'T' and 'Z'

4           and object_type is not null

5 order by owner DESC, object_type DESC;

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=82 Card=11395 Bytes=170925)

Oracle会往前读索引。这个计划最后没有排序步骤:数据已经是有序的。不过,如果你有一组列,其中一些列按升序排序(ASC),另外一些列按降序排序(DESC),此时这种降序索引就能派上用场了,例如:

ops$tkyte@ORA10G> select owner, object_type

2 from t

3 where owner between 'T' and 'Z'

4           and object_type is not null

5 order by owner DESC, object_type ASC;

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=85 Card=11395 Bytes=170925)

11.0            SORT (ORDER BY) (Cost=85 Card=11395 Bytes=170925)

2 1                     INDEX (RANGE SCAN) OF 'T_IDX' (INDEX) (Cost=82 Card=11395 ...

Oracle不能再使用(OWNER, OBJECT_TYPE, OBJECT_NAME)上的索引对数据排序。它可以往前读得到按OWNER DESC排序的数据,但是现在还需要“向后读“来得到按OBJET_TYPE顺序排序(ASC)数据。此时Oracle的实际做法是,它会把所有行收集起来,然后排序。但是如果使用DESC索引,则有:

ops$tkyte@ORA10G> create index desc_t_idx on t(owner desc,object_type asc);

Index created.

ops$tkyte@ORA10G> exec dbms_stats.gather_index_stats( user, 'DESC_T_IDX' );

PL/SQL procedure successfully completed.

 

ops$tkyte@ORA10G> select owner, object_type

2 from t

3 where owner between 'T' and 'Z'

4           and object_type is not null

5 order by owner DESC, object_type ASC;

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2 Card=11395 Bytes=170925)

11.0            INDEX (RANGE SCAN) OF 'DESC_T_IDX' (INDEX) (Cost=2 Card=11395 ...

现在,我们又可以读取有序的数据了,在这个计划的最后并没有额外的排序步骤。应当注意,除非init.ora中的compatible参数设置为8.11.0或更高,否则CREATE INDEX上的DESC选项会被悄悄地忽略,没有警告,也不会产生错误,因为这是先前版本的默认行为。

注意      查询中最好别少了ORDER BY。即使你的查询计划中包含一个索引,但这并不表示数据会以“某种顺序“返回。要想从数据库以某种有序的顺序获取数据,惟一的办法就是在查询中包括一个ORDER BY子句。ORDER BY是无可替代的。

11.2.4             什么情况下应该使用B*树索引?

我并不盲目地信息“经验“(所有规则都有例外),所以,对于什么时候该使用(和不该使用)B*树索引,我没有什么经验可以告诉你。为了说明为什么在这方面不能提供经验,下面给出两种做法,这两种做法同等有效:

q         仅当要通过索引访问表中很少的一部分行(只占一个很小的百分比)时,才使用B*树在列上建立索引。

q         如果要处理表中的多行,而且可以使用索引而不用表,就可以使用一个B*树索引。

q         这两个规则看上去彼此存在冲突,不过在实际中,它们并不冲突,只是涵盖了两种完全不同的情况。根据以上建议,有两种使用索引的方法:

q         索引用于访问表中的行:通过读索引来访问表中的行。此时你希望访问表中很少的一部分行(只占一个很小的百分比)。

q         索引用于回答一个查询:索引包含了足够的信息来回答整个查询,我根本不用去访问表。在这种情况下,索引则用作一个“较瘦“版本的表。

此外,还有其他的一些做法,例如我们还可以使用一个索引来获取表中的所有行,包括索引本身中没有的列。这看上去好像与前面的经验相左。交互式应用中可能就是这种情况,你要得到并显示一些行,然后再得到一些行,如此继续。为此,你可能希望优化查询以使最初的响应时间很短,而不是针对吞吐量进行优化。

第一种情况(也就是说,为了访问表中很少的一部分行而使用索引)是指,如果你有一个表T(还是使用前面的表T),并有如下的一个查询计划:

ops$tkyte@ORA10G> set autotrace traceonly explain

ops$tkyte@ORA10G> select owner, status

2 from t

3 where owner = USER;

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=3 Card=1947 Bytes=25311)

11.0            TABLE ACCESS (BY INDEX ROWID) OF 'T' (TABLE) (Cost=3 Card=1947 ....

2 1                     INDEX (RANGE SCAN) OF 'DESC_T_IDX' (INDEX) (Cost=2 Card=8)

那你就应该只访问这个表的很少一部分行(只占一个很小的百分比)。这里要注意TABLE ACCESS BY INDEX ROWID后面的INDEX (RANGE SCAN)。这说明,Oracle会读索引,然后会对索引条目执行一个数据库块读(逻辑或物理I/O)来得到行数据。如果你要通过索引访问T中的大量行(占很大的百分比),这就不是最高效的方法了(稍后我们将定义多少才算是大百分比)。

在第二种情况下(也就是说,可以用索引而不必用表),你可以通过索引处理表中100%的行(或者实际上可以是任何比例)。使用索引可以只是为了创建一个“较瘦“版本的表。以下查询演示了这个概念:

ops$tkyte@ORA10G> select count(*)

2 from t

3 where owner = user;

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=11. Card=11.Bytes=6)

11.0            SORT (AGGREGATE)

2 1                     INDEX (RANGE SCAN) OF 'T_IDX' (INDEX) (Cost=11. Card=1947

在此,只使用了索引来回答查询,现在访问多少行都没关系,因为我们只会使用索引。从查询计划可以看出,这里从未访问底层表;我们只是扫描了索引结构本身。

重要的是,要了解这两个概念之间的区别。如果必须完成TABLE ACCESS BY INDEX ROWID,就必须确保只访问表中很少的一部分块(只占很小的百分比),这通常对应为很少的一部分行,或者需要能够尽可能快地获取前面的几行(最终用户正在不耐烦地等着这几行)。如果我们访问的行太多(所占百分比过大,不在总行数的1%~20%之间),那么与全表扫描相比,通过B*树索引来访问这些数据通常要花更长的时间。

对于第二种类型的查询,即答案完全可以在索引中找到,情况就完全不同了。我们会读一个索引块,选出许多“行“来处理,然后再到另一个索引块,如此继续,在此从不访问表。某些情况下,还可以在索引上执行一个快速全面扫描,从而更快地回答这类查询。快速全面扫描是指,数据库不按特定的顺序读取索引块,而只是开始读取它们。这里不再是把索引只用作一个索引,此时索引更像是一个表。如果采用快速全面扫描,将不再按索引条目的顺序来得到行。

一般来讲,B*树索引会放在频繁使用查询谓词的列上,而且我们希望从表中只返回少量的数据(只占很小的百分比),或者最终用户请求立即得到反馈。在一个瘦(thin)表(也就是说,只有很少的几个列,或者列很小)上,这个百分比可能相当小。使用这个索引的查询应该只获取表中2%3%(或者更少)的行。在一个胖(fat)表中(也就是说,这个表有很多列,或者列很宽),百分比则可能会上升到表的20%25%。以上建议不一定直接适用于每一个人;这个比例并不直观,但很精确。索引按索引键的顺序存储。索引会按键的有序顺序进行访问。索引指向的块则随机地存储在堆中。因此,我们通过索引访问表时,会执行大量分散、随机的I/O。这里“分散“(scattered)是指,索引会告诉我们读取块1,然后是块11.000、块205、块321、块1、块11.032、块1,等等,它不会要求我们按一种连续的方式读取块1、然后是块2,接着是块3.我们将以一种非常随意的方式读取和重新读取块。这种块I/O可能非常慢。

下面来看这样一个简化的例子,假设我们通过索引读取一个瘦表,而且要读取表中20%的行。若这个表中有100,000行,其中的20%就是2,000行。如果行大小约为80字节,在一个块大小为8KB的数据库中,每个块上则有大约100行。这说明,这个表有大约11.000个块。了解了执行情况,计算起来就非常容易了。我们要通过索引读取20,000行;这说明,大约是20,000TABLE ACCESS BY ROWID操作。为此要处理20,000个表块来执行这个查询。不过,整个表才有大约11.000个块!最后会把表中的每一个块读取好处理20次。即使把行的大小提高一个数量级,达到每行800字节,这样每块有11.行,现在表中就有11.,000个块。要通过索引访问20,000行,仍要求我们把每一个块平均读取2次。在这种情况下,全表扫描就比使用索引高效得多,因为每个块只会命中一次。如果查询使用这个索引来访问数据,效率都不会高,除非对于800字节的行,平均只访问表中不到5%的数据(这样一来,就只会访问大约5,000个块),如果是80字节的行,则访问的数据应当只占更小的百分比(大约0.5%或更少)。

1.      物理组织

数据在磁盘上如何物理地组织,对上述计算会有显著影响,因为这会大大影响索引访问的开销有多昂贵(或者有多廉价)。假设有一个表,其中的行主键由一个序列来填充。向这个表增加数据时,序列号相邻的行一般存储位置也会彼此“相邻“。

注意      如果使用诸如ASSM或多个freelist/freelist组等特性,也会影响数据在磁盘上的组织。这些特性力图将数据发布开,这样可能就观察不到按主键组织的这种自然聚簇。

表会很自然地按主键顺序聚簇(因为数据或多或少就是已这种属性增加的)。当然,它不一定严格按照键聚簇(要想做到这一点,必须使用一个IOT),但是,一般来讲,主键值彼此接近的行的物理位置也会“靠“在一起。如果发出以下查询:

select * from T where primary_key between :x and :y

你想要的行通常就位于同样的块上。在这种情况下,即使要访问大量的行(占很大的百分比),索引区间扫描可能也很有用。原因在于:我们需要读取和重新读取的数据库块很可能会被缓存,因为数据共同放置在同一个位置(co-located)。另一方面,如果行并非共同存储在一个位置上,使用这个索引对性能来讲可能就是灾难性的。只需一个小小的演示就能说明这一点。首先创建一个表,这个表主要按其主键排序::

ops$tkyte@ORA10G> create table colocated ( x int, y varchar2(80) );

Table created.

 

ops$tkyte@ORA10G> begin

2           for i in 1 .. 100000

3           loop

4                  insert into colocated(x,y)

5                  values (i, rpad(dbms_random.random,75,'*') );

6           end loop;

7 end;

8 /

PL/SQL procedure successfully completed.

 

ops$tkyte@ORA10G> alter table colocated

2 add constraint colocated_pk

3 primary key(x);

Table altered.

 

ops$tkyte@ORA10G> begin

2 dbms_stats.gather_table_stats( user, 'COLOCATED', cascade=>true );

3 end;

4 /

PL/SQL procedure successfully completed.

这个表正好满足前面的描述,即在块大小为8KB的一个数据库中,每块有大约100行。在这个表中,,X = 11.2,3的行极有可能在同一个块上。仍取这个表,但有意地使它“无组织“。在COLOCATED表中,我们创建一个Y列,它带有一个前导随机数,现在利用这一点使得数据”无组织“,即不再按主键排序:

ops$tkyte@ORA10G> create table disorganized

2 as

3 select x,y

4 from colocated

5 order by y;

Table created.

 

ops$tkyte@ORA10G> alter table disorganized

2 add constraint disorganized_pk

3 primary key (x);

Table altered.

 

ops$tkyte@ORA10G> begin

2 dbms_stats.gather_table_stats( user, 'DISORGANIZED', cascade=>true );

3 end;

4 /

PL/SQL procedure successfully completed.

可以证明,这两个表是一样的——这是一个关系数据库,所以物理组织对于返回的答案没有影响(至少关于数据库理论的课程中就是这么教的)。实际上,尽管会返回相同的答案,但这两个表的性能特征却有着天壤之别。给定同样的问题,使用同样的查询计划,查看TKPROFSQL跟踪)输出,可以看到以下报告:

select * from colocated where x between 20000 and 40000

call            count        cpu    elapsed            disk      query     current          rows

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

Parse                5       0.00           0.00                 0              0               0                 0

Execute            5       0.00           0.00                 0              0               0                 0

Fetch         6675       0.59           0.60                 0     14495               0     100005

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

total            6685       0.59           0.60                 0     14495               0     100005

 

Rows Row Source Operation

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

20001      TABLE ACCESS BY INDEX ROWID COLOCATED (cr=2899 pr=0 pw=0 time=120134 us)

20001      INDEX RANGE SCAN COLOCATED_PK (cr=1374 pr=0 pw=0 time=40081 us)(object id...

********************************************************************************

select /*+ index( disorganized disorganized_pk ) */* from disorganized

where x between 20000 and 40000

call            count        cpu    elapsed            disk      query     current          rows

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

Parse                5       0.00           0.00                 0              0               0                 0

Execute            5       0.00           0.00                 0              0               0                 0

Fetch         6675       0.85           0.87                 0   106815               0     100005

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

total            6685      0. 85           0.87                 0   106815               0     100005

 

Rows Row Source Operation

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

20001      TABLE ACCESS BY INDEX ROWID DISORGANIZED (cr=21363 pr=0 pw=0 time=220144 ...

20001      INDEX RANGE SCAN DISORGANIZED_PK (cr=1374 pr=0 pw=0 time=40311 us)(...

注意      我把每个查询运行了5次,从而得到每个查询的“平均“运行时间。

简直是难以置信。物理数据布局居然会带来这么大的差异!表11.-5对这些结果做了一个总结。

11.-5 研究物理数据布局对索引访问开销的影响

                                                                                         CPU时间                          逻辑I/O

共同放置(Co-located                                                    0.59                            11.,495

无组织(Disorganized                                                    0.85                           106,815

共同放置表与无组织表的开销相对百分比                              70%                                11.%

在我的数据库中(块大小为8KB),这些表的总块数如下:

ops$tkyte@ORA10G> select table_name, blocks

2 from user_tables

3 where table_name in ( 'COLOCATED', 'DISORGANIZED' );

 

TABLE_NAME                  BLOCKS

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

COLOCATED                          1252

DISORGANIZED                     1219

对无组织表的查询正如先前计算的一样:我们做了20,000次以上的逻辑I/O(这里总共查询了100,000个块,因为查询运行了5次,所以每次查询做了100,000/5 = 20,000次逻辑I/O)。每个块被处理了20次!另一方面,对于物理上共同放置的数据(COLOCATED),逻辑I/O次数则大幅下降。由此很好地说明了为什么很难提供经验:在某种情况下,使用这个索引可能很不错,但在另外一种情况下它却不能很好地工作。从生产系统转储数据并加载到开发系统时也可以考虑这一点,因为这至少能从一定程度上回答如下问题:“为什么在这台机器上运行得完全不同?难道它们不一样吗?“不错,他们确实不一样。

注意      6章曾说过,逻辑I/O会增加,但这还只是冰山一角。每个逻辑I/O都涉及缓冲区缓存的一个或多个锁存器。在一个多用户/CPU情况下,在我们自旋并等待锁存器时,与第一个查询相比,第二个查询所用的CPU时间无疑会高出几倍。第二个示例查询不仅要完成更多的工作,而且无法像第一个查询那样很好地扩展。

ARRAYSIZE对逻辑I/O的影响

有一个问题很有意思,这就是ARRAYSIZE对所执行逻辑I/O的影响。ARRAYSIZE是客户请求下一行时Oracle向客户返回的行数。客户将缓存这些行,在向数据库请求下一个行集之前会先使用缓存的这些行,ARRAYSIZE对查询执行的逻辑I/O可能有非常重要的影响,这是因为,如果必须跨数据库调用反复地访问同一个块(也就是说,通过多个数据库调用反复访问同一个块,这里特别是指跨获取调用),Oracle就必须一而再、再而三地从缓冲区缓存获取这个块。因此,如果一个调用从数据库请求100行,Oracle可能就能够处理完一个数据库块,而无需再次获取这个块。如果你一次请求11.行,Oracle就必须反复地获得同一个块来获取同样的行集。

在这一节前面的例子中,我们使用了SQL*Plus的默认批量获取大小(11.行,如果把获取的总行数除以获取调用的个数,所得到的结果将非常接近11.)。我们在每次获取11.行和每次获取100行的情况下执行前面的查询,从而做一个比较,会观察到对于COLOCATED表有以下结果:

select * from colocated a15 where x between 20000 and 40000

Rows Row Source Operation

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

20001 TABLE ACCESS BY INDEX ROWID COLOCATED (cr=2899 pr=0 pw=0 time=120125...

20001 INDEX RANGE SCAN COLOCATED_PK (cr=1374 pr=0 pw=0 time=40072 us)(...

 

select * from colocated a100 where x between 20000 and 40000

Rows Row Source Operation

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

20001 TABLE ACCESS BY INDEX ROWID COLOCATED (cr=684 pr=0 pw=0 ...)

20001 INDEX RANGE SCAN COLOCATED_PK (cr=245 pr=0 pw=0 ...

执行第一个查询时ARRAYSIZE11.Row Source Operation中(cr-nnnn)值显示出,对这个索引执行了11.374个逻辑I/O,并对表执行了11.625个逻辑I/O2,899-11.374;这些数在Row Source Operation步骤中加在了一起,即2,899)。把ARRAYSIZE11.增加到100时,对索引的逻辑I/O数减至245,这是因为,不必每11.行就从缓冲区缓存重新读取索引叶子块,而是每100行才读取一次。为了说明这一点,假设每个叶子块上能存储200行。如果扫描索引时每次只能读取11.行,则必须把第一个叶子块获取11.次才能得到其中的全部200行。另一方面,如果每次批量获取100行,只需要将这个叶子块从缓冲区缓存中获取两次就能得到其中的所有行。

对于表块也存在这种情况。由于表按索引键同样的顺序排序,所以会更少地获取各个表块,原因是每个获取调用能从表中得到更多的行。

这么说来,如果增加ARRAYSIZECOLOCATED表很合适,那么这对于DISORGANIZED表也同样会很好,是这样吗?并非如此。DISORGANIZED表的相应结果如下所示:

select /*+ index( a15 disorganized_pk ) */ *

from disorganized a15 where x between 20000 and 40000

 

Rows Row Source Operation

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

20001 TABLE ACCESS BY INDEX ROWID DISORGANIZED (cr=21357 pr=0 pw=0 ...

20001 INDEX RANGE SCAN DISORGANIZED_PK (cr=1374 pr=0 pw=0 ...

 

select /*+ index( a100 disorganized_pk ) */ *

from disorganized a100 where x between 20000 and 40000

 

Rows Row Source Operation

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

20001 TABLE ACCESS BY INDEX ROWID OBJ#(75652) (cr=20228 pr=0 pw=0 ...

20001 INDEX RANGE SCAN OBJ#(75653) (cr=245 pr=0 pw=0 time=20281 us)(...

索引有关的结果并无不同,这是有道理的,因为不论表如何组织,索引中存储的数据都一样,执行一次查询时,如果从11.增加到100,对索引执行的逻辑I/O总次数在调整ARRAYSIZE前后并没有太大差别:分别是21,35720,281。为什么呢?原因是对表执行的逻辑I/O次数根本没有变化,如果把每个查询执行的逻辑I/O总次数减去对索引执行的逻辑I/O次数,就会发现这两个查询对表执行的逻辑I/O此时都是11.,983。这是因为,每次我们希望从数据库得到N行时,其中的任何两行在同一个块上的机率非常小,所以通过一个调用就从一个表块中得到多行是不可能的。

我见过的每一种与Oracle交互的专业编程语言都实现了这种批量获取(array fetching)的概念。在PL/SQL中,可以使用BULK COLLECT,也可以依靠为隐式游标for循环执行的隐式批量获取(一次100行)来实现。在Java/JDBC中,连接(connect)或语句(statement)对象上有一个预获取(prefetch)方法。Oracle调用接口(Oracle Call Interface,即OCI,这是一个C API)与Pro*C类似,允许在程序中设置预获取大小。可以看到,这对查询执行的逻辑I/O次数会有重要而且显著的影响,值得注意。

在这个例子的最后,来看一下全面扫描DISORGANIZED表时会发生什么:

select * from disorganized where x between 20000 and 40000

 

call            count        cpu    elapsed            disk      query     current          rows

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

Parse                5       0.00           0.00                 0              0               0                 0

Execute            5       0.00           0.00                 0              0               0                 0

Fetch         6675       0.53           0.54                 0     12565               0     100005

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

total            6685       0.53       0. 54 0       12565              0   100005

Rows Row Source Operation

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

20001 TABLE ACCESS FULL DISORGANIZED (cr=2513 pr=0 pw=0 time=60115 us)

由此显示出,在这个特殊的例子中,根据数据在磁盘上的物理存储的方式,非常合适采用全表扫描。这就带来一个问题:“为什么优化器不先对这个查询执行全面扫描呢?“这么说吧,如果按照它自己本来的设计,确实会先执行全面扫描,但是在对DISORGANIZED的第一个示例查询中,我有意地为查询提供了提示,告诉优化器要构造一个使用这个索引的计划。在第二个例子中,则是让优化器自己来选择最佳的字体计划。

2.      聚簇因子

接下来,我们来看Oracle所用的一些信息。我们要特别查看USER_INDEXES视图中的CLUSTERING_FACTOR列。Oracle reference手册指出了这个列有以下含义:

根据索引的值指示表中行的有序程度:

q         如果这个值与块数接近,则说明表相当有序,得到了很好的组织,在这种情况下,同一个叶子块中的索引条目可能指向同一个数据块上的行。

q         如果这个值与行数接近,表的次序可能就是非常随机的。在这种情况下,同一个叶子块上的索引条目不太可能指向同一个数据块上的行。

可以把聚簇因子(clustering factor)看作是通过索引读取整个表时对表执行的逻辑I/O次数。也就是说,CLUSTERING_FACTOR指示了表相对于索引本身的有序程度,查看这些索引时,会看到以下结果:

ops$tkyte@ORA10G> select a.index_name,

2           b.num_rows,

3           b.blocks,

4           a.clustering_factor

5 from user_indexes a, user_tables b

6 where index_name in ('COLOCATED_PK', 'DISORGANIZED_PK' )

7           and a.table_name = b.table_name

8 /

INDEX_NAME          NUM_ROWS       BLOCKS      CLUSTERING_FACTOR

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

COLOCATED_PK              100000              1252                                          1190

DISORGANIZED_PK         100000              1219                                       99932

注意      对于这一节的例子,我使用了一个ASSM管理的表空间,由此可以解释为什么COLOCATED表的聚簇因子小于表中的块数。COLOCATED表中在HWM之下有一些未格式化的块,其中未包含数据,而且ASSM本身也使用了一些块来管理空间,索引区间扫描中不会读取这些块。第11.章更详细地解释了HWMASSM

所以数据库说:“如果通过索引COLOCATED_PK从头到尾地读取COLOCATED表中的每一行,就要执行11.190I/O。不过,如果我们对DISORGANIZED表做同样的事情,则会对这个表执行99,932I/O。“之所以存在这么大的区别,原因在于,当Oracle对索引结构执行区间扫描时,如果它发现索引中的下一行于前一行在同一个数据库块上,就不会再执行另一个I/O从缓冲区缓存中获得表块。它已经有表块的一个句柄,只需直接使用就可以了。不过,如果下一行不在同一个块上,就会释放当前的这个块,而执行另一个I/O从缓冲区缓存获取要处理的下一个块。因此,在我们对索引执行区间扫描时,COLOCATED_PK索引会发现下一行几乎总于前一行在同一个块上。DISORGANIZED_PK索引发现的情况则恰好相反。实际上,你会看到这个测量相当准确。通过使用提示,让优化器使用索引全面扫描来读取整个表,再统计非NULLY值个数,就能看到通过索引读取整个表需要执行多少次I/O

select count(Y)

from

      (select /*+ INDEX(COLOCATED COLOCATED_PK) */ * from colocated)

 

call    count        cpu    elapsed        disk         query      current          rows

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

Parse        1       0.00           0.00             0                 0                 0                 0

Execute    1       0.00           0.00             0                 0                 0                 0

Fetch        2       0.11.           0.11.             0          1399                 0                 1

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

total           4       0.11.           0.11.             0          1399                 0                 1

   Rows    Row Source Operation

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

           1    SORT AGGREGATE (cr=1399 pr=0 pw=0 time=160325 us)

100000    TABLE ACCESS BY INDEX ROWID COLOCATED (cr=1399 pr=0 pw=0 time=500059 us)

100000    INDEX FULL SCAN COLOCATED_PK (cr=209 pr=0 pw=0 time=101057 us)(object ...

********************************************************************************

select count(Y)

from

      (select /*+ INDEX(DISORGANIZED DISORGANIZED_PK) */ * from disorganized)

 

call    count        cpu    elapsed        disk         query      current          rows

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

Parse        1       0.00           0.00             0                 0                 0                 0

Execute    1       0.00           0.00             0                 0                 0                 0

Fetch        2       0.34           0.40             0     100141                 0                 1

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

total           4       0.34           0.40             0     100141                 0                 1

   Rows   Row Source Operation

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

           1   SORT AGGREGATE (cr=100141 pr=0 pw=0 time=401109 us)

100000   TABLE ACCESS BY INDEX ROWID OBJ#(66615) (cr=100141 pr=0 pw=0 time=800058...

100000   INDEX FULL SCAN OBJ#(66616) (cr=209 pr=0 pw=0 time=101129 us)(object...

在这两种情况下,索引都需要执行209次逻辑I/ORow Source Operation行中的cr-209)。如果将一致读(consistent read)总次数减去209,只测量对表执行的I/O次数,就会发现所得到的数字与各个索引的聚簇因子相等。COLOCATED_PK是“有序表“的一个经典例子,DISORGANIZE_PK则是一个典型的”表次序相当随机“的例子。现在来看看这对优化器有什么影响。如果我们想获取25,000行,Oracle对两个索引都会选择全表扫描(通过索引获取25%的行不是最优计划,即使是对很有序的表也是如此)。不过,如果只选择表数据的11.%,就会观察到以下结果:


ops$tkyte@ORA10G> select * from colocated where x between 20000 and 30000;

 

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=143 Card=10002 Bytes=800160)

11.0            TABLE ACCESS (BY INDEX ROWID) OF 'COLOCATED' (TABLE) (Cost=143 ...

2 1                     INDEX (RANGE SCAN) OF 'COLOCATED_PK' (INDEX (UNIQUE)) (Cost=22 ...

 

ops$tkyte@ORA10G> select * from disorganized where x between 20000 and 30000;

 

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=337 Card=10002 Bytes=800160)

11.0            TABLE ACCESS (FULL) OF 'DISORGANIZED' (TABLE) (Cost=337 Card=10002 ...

这里的表结构和索引与前面完全一样,但聚簇因子有所不同。在这种情况下,优化器为COLOCATED表选择了一个索引访问计划,而对DISORGANIZED表选择了一个全面扫描访问计划。要记住,11.%并不是一个阀值,它只是一个小于25%的数,而且在这里会导致对COLOCATED表的一个索引区间扫描。

以上讨论的关键点是,索引并不一定总是合适的访问方法。优化器也许选择不使用索引,而且如前面的例子所示,这种选择可能很正确。影响优化器是否使用索引的因素有很多,包括物理数据布局。因此,你可能会矫枉过正,力图重建所有的表来使所有索引有一个好的聚簇因子,但是在大多数情况下这可能只会浪费时间。只有当你在对表中的大量数据(所占百分比很大)执行索引区间扫描时,这才会产生影响。另外必须记住,对于一个表来说,一般只有一个索引能有合适的聚簇因子!表中的行可能只以一种方式排序。在前面所示的例子中,如果Y列上还有一个索引,这个索引在COLOCATED表中可能就不能很好地聚簇,而在DISORGANIZED表中则恰好相反。如果你认为数据物理聚簇很重要,可以考虑使用一个IOTB*树聚簇,或者在连续地重建表时考虑散列聚簇。

11.2.5             B*树小结

B*树索引是到目前为止Oracle数据库中最常用的索引结构,对B*树索引的研究和了解也最为深入。它们是绝好的通用索引机制。在访问时间方面提供了很大的可扩缩性,从一个11.000行的索引返回数据所用的时间与一个100,000行的索引结构中返回数据的时间是一样的。

什么时候建立索引,在哪些列上建立索引,你的设计中必须注意这些问题。索引并不一定就意味着更快的访问;实际上你会发现,在许多情况下,如果Oracle使用索引,反而会使性能下降。这实际上两个因素的一个函数,其中一个因素是通过索引需要访问表中多少数据(占多大的百分比),另一个因素是数据如何布局。如果能完全使用索引“回答问题“(而不用表),那么访问大量的行(占很大的百分比)就是有意义的,因为这样可以避免读表所带来的额外的分散I/O。如果使用索引来访问表,可能就要确保只处理整个表中的很少一部分(只占很小的百分比)。

应该在应用的设计期间考虑索引的设计和实现,而不要事后才想起来(我就经常见到这种情况)。如果对如何访问数据做了精心的计划和考虑,大多数情况下就能清楚地知道需要什么索引。

11.3   位图索引

位图索引(bitmap index)是从Oracle7.3版本开始引入的。目前Oracle企业版和个人版都支持位图索引,但标准版不支持。位图索引是为数据仓库/即席查询环境设计的,在此所有查询要求的数据在系统实现时根本不知道。位图索引特别不适用于OLTP系统,如果系统中的数据会由多个并发会话频繁地更新,这种系统也不适用位图索引。

位图索引是这样一种结构,其中用一个索引键条目存储指向多行的指针;这与B*树结构不同,在B*树结构中,索引键和表中的行存在着对应关系。在位图索引中,可能只有很少的索引条目,每个索引条目指向多行。而在传统的B*树中,一个索引条目就指向一行。

下面假设我们要在EMP表的JOB列上创建一个位图索引,如下:

Ops$tkyte@ORA10G> create BITMAP index job_idx on emp(job);

Index created.

Oracle在索引中存储的内容如表11.-6所示。

11.-6 Oracle如何存储JOB-IDX位图索引

/            1    2    3    4    5    6    7    8    9    11. 11. 11. 11. 11.

ANALYST      0    0    0    0    0    0    0    1    0    1    0    0    1    0

CLERK          1    0    0    0    0    0    0    0    0    0    1    1    0    1

MANAGER   0    0    0    1    0    1    1    0    0    0    0    0    0    0

PRESIDENT 0    0    0    0    0    0    0    0    1    0    0    0    0    0

SALESMAN   0    1    1    0    1    0    0    0    0    0    0    0    0    0

11.-6显示了第811.11.行的值为ANALYST,而第467行的值为MANAGER。在此还显示了所有行都不为null(位图索引可以存储null条目;如果索引中没有null条目,这说明表中没有null行)。如果我们想统计值为MANAGER的行数,位图索引就能很快地完成这个任务。如果我们想找出JOBCLERKMANAGER的所有行,只需根据索引合并它们的位图,如表11.-7所示。

11.-7 OR的表示

/            1    2    3    4    5    6    7    8    9    11. 11. 11. 11. 11.

CLERK          1    0    0    0    0    0    0    0    0    0    1    1    0    1

MANAGER   0    0    0    1    0    1    1    0    0    0    0    0    0    0

CLERK     1    0    0    1    0    1    1    0    0    0    1    1    0    1

MANAGER

11.-7清楚地显示出,第146711.11.11.行满足我们的要求。Oracle如下为每个键值存储位图,使得每个位置表示底层表中的一个rowid,以后如果确实需要访问行时,可以利用这个rowid进行处理。对于以下查询:

select count(*) from emp where job = 'CLERK' or job = 'MANAGER'

用位图索引就能直接得出答案。另一方面,对于以下查询:

select * from emp where job = 'CLERK' or job = 'MANAGER'

则需要访问表。在此Oracle会应用一个函数把位图中的第i位转换为一个rowid,从而可用于访问表。

11.3.1             什么情况下应该使用位图索引?

位图索引对于相异基数(distinct cardinality)低的数据最为合适(也就是说,与职工数据集的基数相比,这个数据只有很少几个不同的值)。对此做出量化是不太可能的——换句话说,很难定义低相异基数到底是多大。在一个有几千条记录的数据集中,2就是一个低相异基数,但是在一个只有两行的表中,2就不能算是低相异基数了。而在一个有上千万或上亿条记录的表中,甚至100,000都能作为一个低相异基数。所以,多大才算是低相异基数,这要相对于结果集的大小来说。这是指,行集中不同项的个数除以行数应该是一个很小的数(接近于0)。例如,GENDER列可能取值为MFNULL。如果一个表中有20,000条员工记录,那么3/20000=0.00015。类似地,如果有100,000个不同的值,与11.,000,000条结果相比,比值为0.01,同样这也很小(可算是低相异基数)。这些列就可以建立位图索引。它们可能不适合建立B*树索引,因为每个值可能会获取表中的大量数据(占很大百分比)。如前所述,B*树索引一般来讲应当是选择性的。与之相反,位图索引不应是选择性的,一般来讲它们应该“没有选择性“。

如果有大量即席查询,特别是查询以一种即席方式引用了多列或者会生成诸如COUNT之类的聚合,在这样的环境中,位图索引就特别有用。例如,假设你有一个很大的表,其中有3列:GENDERLOCATIONAGE_GROUP。在这个表中,GENDER只有两个可取值:MFLOCATION可取值为11.50AGE_GROUP是一个代码,分别表示11. and under11.及以下)、11.-2526-3031-4041 and over41及以上)。你必须支持大量即席查询,形式如下:

Select count(*)

from T

where gender = 'M'

         and location in ( 1, 11., 30 )

         and age_group = '41 and over';

 

select *

from t

where ( ( gender = 'M' and location = 20 )

                  or ( gender = 'F' and location = 22 ))

         and age_group = '11. and under';

 

select count(*) from t where location in (11.,20,30);

 

select count(*) from t where age_group = '41 and over' and gender = 'F';

你会发现,这里使用传统的B*树索引机制是不行的。如果你想使用一个索引来得到答案,就需要组合至少36个可能的B*树索引,才能通过索引访问数据。由于这3列或它们的任何子集都有可能出现,所以需要在以下列上建立很大的串联B*树索引:

q         GENDERLOCATIONAGE_GROUP:对应使用了这3列、使用了GENDERLOCATION或者仅使用GENDER的查询。

q         LOCATIONAGE_GROUP:对应使用了LOCATIONAGE_GROUP或者仅使用LOCATION的查询。

q         AGE_GROUPGENDER:对应使用了AGE_GROUPGENDER或者仅使用LOCATION的查询。

要减少搜索的数据量,还可以有其他排列,以减少所扫描索引结构的大小。这是因为在此忽略了这样一个重要事实:对这种低基数数据建立B*树索引并不明智。

这里位图索引就能派上用场了。利用分别建立在各个列上的3个较小的位图索引,就能高效地满足前面的所有谓词条件。对于引用了这3列(其中任何一例及任何子集)的任何谓词,Oracle只需对3个索引的位图使用函数ANDORNOT,就能得到相应的结果集。它会得到合并后的位图,如果必要还可以将位图中的“1”转换为rowid来访问数据(如果只是统计与条件匹配的行数,Oracle就只会统计“1”位的个数)。下面来看一个例子。首先,生成一些测试数据(满足我们指定的相异基数),建立索引,并收集统计。我们将利用DBMS_RANDOM包来生成满足我们的分布要求的随机数据:

ops$tkyte@ORA10G> create table t

2 ( gender not null,

3 location not null,

4 age_group not null,

5 data

6 )

7 as

8 select decode( ceil(dbms_random.value(11.2)),

9           1, 'M',

11.         2, 'F' ) gender,

11.         ceil(dbms_random.value(11.50)) location,

11.         decode( ceil(dbms_random.value(11.5)),

11.         1,'11. and under',

11.         2,'11.-25',

11.         3,'26-30',

11.         4,'31-40',

11.         5,'41 and over'),

11.         rpad( '*', 20, '*')

11. from big_table.big_table

20 where rownum <= 100000;

Table created.

 

ops$tkyte@ORA10G> create bitmap index gender_idx on t(gender);

Index created.

 

ops$tkyte@ORA10G> create bitmap index location_idx on t(location);

Index created.

 

ops$tkyte@ORA10G> create bitmap index age_group_idx on t(age_group);

Index created.

 

ops$tkyte@ORA10G> exec dbms_stats.gather_table_stats( user, 'T', cascade=>true );

PL/SQL procedure successfully completed.

现在来看前面各个即席查询的相应查询计划:

ops$tkyte@ORA10G> Select count(*)

2 from T

3 where gender = 'M'

4     and location in ( 1, 11., 30 )

5     and age_group = '41 and over';

 

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=5 Card=11.Bytes=11.)

11.0       SORT (AGGREGATE)

2 1           BITMAP CONVERSION (COUNT) (Cost=5 Card=11.Bytes=11.)

3 2               BITMAP AND

4 3                   BITMAP INDEX (SINGLE VALUE) OF 'GENDER_IDX' (INDEX (BITMAP))

5 3                   BITMAP OR

6 5                       BITMAP INDEX (SINGLE VALUE) OF 'LOCATION_IDX' (INDEX (BITMAP))

7 5                       BITMAP INDEX (SINGLE VALUE) OF 'LOCATION_IDX' (INDEX (BITMAP))

8 5                       BITMAP INDEX (SINGLE VALUE) OF 'LOCATION_IDX' (INDEX (BITMAP))

9 3                   BITMAP INDEX (SINGLE VALUE) OF 'AGE_GROUP_IDX' (INDEX (BITMAP))

这个例子展示出了位图索引的强大能力。Oracle能看到location in (11.11.,30),知道要读取这3个位置(对于这3个值的位置)上的索引,并在位图中对这些“位”执行逻辑OR。然后将得到的位图与AGE_GROUP=’41 AND OVER’GENDER=’M’的相应位图执行逻辑AND。再统计1”的个数,这就得到了答案:

ops$tkyte@ORA10G> select *

2 from t

3 where ( ( gender = 'M' and location = 20 )

4           or ( gender = 'F' and location = 22 ))

5           and age_group = '11. and under';

 

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=77 Card=507 Bytes=16731)

11.0       TABLE ACCESS (BY INDEX ROWID) OF 'T' (TABLE) (Cost=77 Card=507 ...

2 1           BITMAP CONVERSION (TO ROWIDS)

3 2               BITMAP AND

4 3                   BITMAP INDEX (SINGLE VALUE) OF 'AGE_GROUP_IDX' (INDEX (BITMAP))

5 3                   BITMAP OR

6 5                       BITMAP AND

7 6                           BITMAP INDEX (SINGLE VALUE) OF 'LOCATION_IDX' (INDEX (BITMAP))

8 6                           BITMAP INDEX (SINGLE VALUE) OF 'GENDER_IDX' (INDEX (BITMAP))

9 5                       BITMAP AND

11. 9                             BITMAP INDEX (SINGLE VALUE) OF 'GENDER_IDX' (INDEX (BITMAP))

11. 9                             BITMAP INDEX (SINGLE VALUE) OF 'LOCATION_IDX' (INDEX (BITMAP))

这个逻辑与前面是类似的,由计划显示:这里执行逻辑OR的两个条件是通过AND适当的位图逻辑计算得到的,然后再对这些结果执行逻辑OR得到一个位图。再加上另一个AND条件(以满足AGE_GROUP=’11.’ AND UNDER),我们就找到了满足所有条件的结果。由于这一次要请求具体的行,所以Oracle会把位图中的各个“1“和”0“转换为rowid,来获取源数据。

在一个数据仓库或支持多个即席SQL查询的大型报告系统中,能同时合理地使用尽可能多的索引确实非常有用。这里不常使用传统的B*树索引,甚至不能使用B*树索引,随着即席查询要搜索的列数的增加,需要的B*树索引的组合也会飞速增长。

不过,在某些情况下,位图并不合适。位图索引在读密集的环境中能很好地工作,但是对于写密集的环境则极不适用。原因在于,一个位图索引键条目指向多行。如果一个会话修改了所索引的数据,那么在大多数情况下,这个索引条目指向的所有行都会被锁定。Oracle无法锁定一个位图索引条目中的单独一位;而是会锁定这个位图索引条目。倘若其他修改也需要更新同样的这个位图索引条目,就会被“关在门外“。这样将大大影响并发性,因为每个更新都有可能锁定数百行,不允许并发地更新它们的位图列。在此不是像你所想的那样锁定每一行,而是会锁定很多行。位图存储在块(chunk)中,所以,使用前面的EMP例子就可以看到,索引键ANALYST在索引中出现了多次,每一次都指向数百行。更新一行时,如果修改了JOB列,则需要独占地访问其中两个索引键条目:对应老值的索引键条目和对应新值的索引键条目。这两个条目指向的数百行就不允许其他会话修改,直到UPDATE提交。

11.3.2             位图联结索引

Oracle9i引入了一个新的索引类型:位图联结索引(bitmap join index)。通常都是在一个表上创建索引,而且只使用这个表的列。位图联结索引则打破了这个规则,它允许使用另外某个表的列对一个给定表建立索引。实际上,这就允许对一个索引结构(而不是表本身)中的数据进行逆规范化。

考虑简单的EMP表和DEPT表。EMP有指向DEPT的一个外键(DEPTNO列)。DEPT表有一个DNAME属性(部门名)。最终用户会频繁地问这样的问题:“销售部门有多少人?“”谁在销售部门工作?“”可以告诉我销售部门中业绩最好的前N个人吗?“注意他们没有这样问:”DEPTNO30的部门中有多少人?“他们没有用这些键值;而是用了人可读的部门名。因此,最后运行的查询如下所示:

select count(*)

from emp, dept

where emp.deptno = dept.deptno

and dept.dname = 'SALES'

/

 

select emp.*

from emp, dept

where emp.deptno = dept.deptno

and dept.dname = 'SALES'

/

使用传统索引的话,这些查询中DEPT表和EMP表都必须访问。我们可以使用DEPT.DNAME上的一个索引来查找SALES行,并获取SALESDEPTNO值,然后使用EMP.DEPTNO上的一个索引来查找匹配的行,但是如果使用一个位图联结索引,就不需要这些了。利用位图联结索引,我们能对DEPT.DNAME列建立索引,但这个索引不是指向DEPT表,而是指向EMP表。这是一个全新的概念:能从其他表对某个表的属性建立索引,而这可能会改变你的报告系统中实现数据模型的方式。实际上,可以鱼和熊掌兼得,一方面保持规范化数据结构不变,与此同时还能得到逆规范化的好处。

以下是我们为这个例子创建的索引:

ops$tkyte@ORA10G> create bitmap index emp_bm_idx

2 on emp( d.dname )

3 from emp e, dept d

4 where e.deptno = d.deptno

5 /

Index created.

注意,这个CREATE INDEX开始看上去很“正常“,它会在表上创建索引INDEX_NAME。但之后就不那么”正常“了。可以看到在此引用了DEPT表中的一列:D.DNAME。这里有一个FROM子句。使这个CREATE INDEX语句有些像查询。另外,多个表之间有一个联结条件。这个CREATE INDEX语句对DEPT.DNAME列建立了索引,但这在EMP表的上下文中。对于前面提到的那些问题,我们会发现数据库根本不会访问DEPT,而且也不需要访问DEPT,因为DNAME列现在是在指向EMP表中的行的索引中。为了便于说明,我们把EMP表和DEPT表制作得看上去很”大“(以避免CBO认为它们很小,以至于选择执行全面扫描,而不是使用索引):

ops$tkyte@ORA10G> begin

2     dbms_stats.set_table_stats( user, 'EMP',

3     numrows => 1000000, numblks => 300000 );

4     dbms_stats.set_table_stats( user, 'DEPT',

5     numrows => 100000, numblks => 30000 );

6 end;

7 /

PL/SQL procedure successfully completed.

然后再执行查询:

ops$tkyte@ORA10G> set autotrace traceonly explain

ops$tkyte@ORA10G> select count(*)

2 from emp, dept

3 where emp.deptno = dept.deptno

4     and dept.dname = 'SALES'

5 /

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=11.Card=11.Bytes=11.)

11.0       SORT (AGGREGATE)

2 1           BITMAP CONVERSION (COUNT) (Cost=11.Card=10000 Bytes=130000)

3 2               BITMAP INDEX (SINGLE VALUE) OF 'EMP_BM_IDX' (INDEX (BITMAP))

可以看到,要回答这个特定的问题,我们不必真正去访问EMP表或DEPT表,答案全部来自索引本身。回答这个问题所需的全部信息都能在索引结构中找到。

另外,我们还能避免访问DEPT表,使用EMP上的索引就能从DEPT合并我们需要的数据,直接访问我们所需的行:

ops$tkyte@ORA10G> select emp.*

2 from emp, dept

3 where emp.deptno = dept.deptno

4     and dept.dname = 'SALES'

5 /

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=6145 Card=10000 Bytes=870000)

11.0       TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (TABLE) (Cost=6145 Card=10000 ...

2 1           BITMAP CONVERSION (TO ROWIDS)

3 2               BITMAP INDEX (SINGLE VALUE) OF 'EMP_BM_IDX' (INDEX (BITMAP))

位图联结索引确实有一个先决条件。联结条件必须联结到另一个表中的主键或惟一键。在前面的例子中,DEPT.DEPTNO就是DEPT表的主键,而且这个主键必须合适,否则就会出现一个错误:

ops$tkyte@ORA10G> create bitmap index emp_bm_idx

2 on emp( d.dname )

3 from emp e, dept d

4 where e.deptno = d.deptno

5 /

from emp e, dept d

*

ERROR at line 3:

ORA-25954: missing primary key or unique constraint on dimension

11.3.3             位图索引小结

如果还犹豫不定,你可以亲自试一试。向一个表(或一组表)增加位图索引很容易,你可以自己看看位图索引会做些什么。另外,创建位图索引通常比创建B*树索引快得多。要查看位图索引是否适用于你的环境,最好的办法就是动手实验。经常有人问我:“怎么定义低基数?“对这个问题并没有直截了当的答案。有时,在100,000行的表中3就是一个低基数;而有时在11.000,000行的表中11.,000也是一个低基数。低基数并不是说不同的值有几位数字。要想知道你的应用中是否适合使用位图,最好做个实验。一般而言,如果是一个很大的环境,主要是只读操作,并且有大量即席查询,你所需要的可能正是一组位图索引。

11.4   基于函数的索引

基于函数的索引(function-based index)是Oracle8.11.5中增加的一种索引。现在这已经是标准版的一个特性,但在Oracle9i Release 2之前的版本中,这还只是企业版的一个特性。

利用基于函数的索引,我们能够对计算得出的列建立索引,并在查询中使用这些索引。简而言之,利用这种能力,你可以做很多事情,如执行大小写无关的搜索或排序;根据复杂的公式进行搜索;还可以实现你自己的函数和运算符,然后在此之上执行搜索从而高效地扩展SQL语言。

使用基于函数的索引可能有很多原因,其中主要的原因如下:

q         使用索引很容易实现,并能立即提交一个值。

q         可以加快现有应用的速度,而不用修改任何逻辑或查询。

11.4.1             重要的实现细节

Oracle9i Release 1中,要创建和使用基于函数的索引,需要一些初始设置,这与B*树和位图索引有所不同。

注意      以下内容只适用于Oracle9i Release 1及以前的版本。在Oracle9i Release2和以后的版本中,基于函数的索引无需初始设置就可以使用。Oracle9i Release 2Oracle SQL Reference手册在这方面说得不对,它称你需要这些权限,但实际上并不需要。

你必须使用一些系统参数或会话设置,而且必须能创建这些系统参数或会话设置,这需要有以下权限:

q         必须有系统权限QUERY REWRITE,从而在你自己的模式中的表上创建基于函数的索引。

q         必须有系统权限GLOBAL QUERY REWRITE,从而在其他模式中的表上创建基于函数的索引。

q         要让优化器使用基于函数的索引,必须设置以下会话或系统变量:QUERY_REWRITE_ENABLED=TRUEQUERY_REWRITE_INTEGRITY=RUSTED。可以在会话级用ALTER SESSION来完成设置,也可以在系统级通过ALTER SYSTEM来设置,还可以在init.ora参数文件中设置。QUERY_REWRITE_ENABLED允许优化器重写查询来使用基于函数的索引。QUERY_REWRITE_INTEGRITY告诉优化器要“相信“程序员标记为确定性的代码确实是确定性的(下一节会给出一些例子来说明什么是确定性代码,并指出确定性代码的含义)。如果代码实际上不是确定性的(也就是说,给定相同的输入,它会返回不同的输出),通过索引获取得到的行可能是不正确的。你必须负责确保定义为确定性的函数确实是确定性的。

在所有版本中,以下结论都适用:

q         使用基于代价的优化器(cost-based optimizerCBO)。在基于函数的索引中,虚拟列(应用了函数的列)只对CBO可见,而基于规则的优化器(rule-based optimizer RBO)不能使用这些虚拟列。RBO可以利用基于函数的索引中未应用函数的前几列。

q         对于返回VARCHAR2RAW  类型的用户编写的函数,使用SUBSTR来约束其返回值,也可以把SUBSTR隐藏在一个视图中(这是推荐的做法)。同样,下一节会给出这样一个例子。

一旦满足前面的条件,基于函数的索引就很容易使用。只需使用CREATE INDEX命令来创建索引,优化器会在运行时发现并使用你的索引。

11.4.2             一个简单的基于函数的索引例子

考虑以下例子。我们想在EMP表的ENAME列上执行一个大小写无关的搜索。在基于函数的索引引入之前,我们可能必须采用另外一种完全不同的方式来做到。可能要为EMP表增加一个额外的列,例如名为UPPER_ENAME的列。这个列由INSERTUPDATE上的一个数据库触发器维护;这个触发器只是设置NEW.UPPER_NAME := UPPER(:NEW.ENAME)。另外要在这个额外的列上建立索引。但是现在有了基于函数的索引,就根本不用再增加额外的列了。

首先在SCOTT模式中创建演示性EMP表的一个副本,并在其中增加一些数据:

ops$tkyte@ORA10G> create table emp

2 as

3 select *

4 from scott.emp

5 where 11.0;

Table created.

 

ops$tkyte@ORA10G> insert into emp

2     (empno,ename,job,mgr,hiredate,sal,comm,deptno)

3     select rownum empno,

4         initcap(substr(object_name,11.11.)) ename,

5         substr(object_type,11.9) JOB,

6         rownum MGR,

7         created hiredate,

8         rownum SAL,

9         rownum COMM,

11.     (mod(rownum,4)+1)*11. DEPTNO

11. from all_objects

11. where rownum < 10000;

9999 rows created.

接下来,在ENAME列的UPPER值上创建一个索引,这就创建了一个大小写无关的索引:

ops$tkyte@ORA10G> create index emp_upper_idx on emp(upper(ename));

Index created.

最后,前面已经提到了,我们要分析这个表。这是因为,需要利用CBO来使用基于函数的索引。在Oracle 10g中,从技术上讲这一步不是必须的,因为就会默认使用CBO,而且动态采样会收集所需的信息,但是最好还是自行收集统计信息。

ops$tkyte@ORA10G> begin

2     dbms_stats.gather_table_stats

3     (user,'EMP',cascade=>true);

4 end;

5 /

PL/SQL procedure successfully completed.

现在就在一个列的UPPER值上建立了一个索引。执行“大小写无关“查询(如以下查询)的任何应用都能利用这个索引:

ops$tkyte@ORA10G> set autotrace traceonly explain

ops$tkyte@ORA10G> select *

2 from emp

3 where upper(ename) = 'KING';

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2 Card=2 Bytes=92)

11.0       TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (TABLE) (Cost=2 Card=2 Bytes=92)

2 1           INDEX (RANGE SCAN) OF 'EMP_UPPER_IDX' (INDEX) (Cost=11.Card=2)

这样就能得到索引所能提供的性能提升。在有这个特性之前,EMP表中的每一行都要扫描、改为大写并进行比较。与之不同,利用UPPER(ENAME)上的索引,查询为索引提供了常量KING,然后只对少量数据执行区间扫描,并按rowid访问表来得到数据。这是相当快的。

对列上的用户编写的函数建立索引时,能更清楚地看到这种性能提升。Oracle 7.1开始允许在SQL中使用用户编写的函数,所以我们可以做下面的工作:

SQL> select my_function(ename)

2 from emp

3 where some_other_function(empno) > 11.

4 /

这很棒,因为现在我们能很好地扩展SQL语言,可以包括应用特定的函数。不过,遗憾的是,有时前面这个查询的性能并不让人满意。假设EMP表中有11.000行。查询期间函数SOME_OTHER_FUNCTION就会执行11.000次,每行执行一次。另外,假设这个函数执行时需要百分之一秒的时间,尽管这个查询相对简单,现在也至少需要11.秒的时间才能完成。

下面来看一个实际的例子,我们在PL/SQL中实现了一个例程,它对SOUNDEX例程稍有修改。另外,我们将使用一个包全局变量作为过程中的一个计数器,这样我们就能执行使用了MY_SOUNDEX函数的查询,并查看这个函数会被调用多少次:

ops$tkyte@ORA10G> create or replace package stats

2 as

3     cnt number default 0;

4 end;

5 /

Package created.

ops$tkyte@ORA10G> create or replace

2     function my_soundex( p_string in varchar2 ) return varchar2

3    deterministic

4     as

5         l_return_string varchar2(6) default substr( p_string, 1, 1 );

6         l_char varchar2(1);

7         l_last_digit number default 0;

8

9         type vcArray is table of varchar2(11.) index by binary_integer;

11.     l_code_table vcArray;

11.

11. begin

11.     stats.cnt := stats.cnt+1;

11.

11.     l_code_table(1) := 'BPFV';

11.     l_code_table(2) := 'CSKGJQXZ';

11.     l_code_table(3) := 'DT';

11.     l_code_table(4) := 'L';

11.     l_code_table(5) := 'MN';

20     l_code_table(6) := 'R';

21

22

23     for i in 1 .. length(p_string)

24     loop

25         exit when (length(l_return_string) = 6);

26         l_char := upper(substr( p_string, i, 1 ) );

27

28         for j in 1 .. l_code_table.count

29         loop

30             if (instr(l_code_table(j), l_char ) > 0 AND j <> l_last_digit)

31             then

32               l_return_string := l_return_string || to_char(j,'fm9');

33               l_last_digit := j;

34             end if;

35         end loop;

36     end loop;

37

38      return rpad( l_return_string, 6, '0' );

39 end;

40 /

Function created.

注意在这个函数中,我们使用了一个新的关键字DETERMINISTIC。这就声明了:前面这个函数在给定相同的输入时,总会返回完全相同的输出。要在一个用户编写的函数上创建索引,这个关键字是必要的。我们必须告诉Oracle这个函数是确定性的(DETERMINISTIC),而且在给定相同输入的情况下总会返回一致的结果。通过这个关键字,就是在告诉Oracle:可以相信这个函数,给定相同的输入,不论做多少次调用,它肯定能返回相同的值。如果不是这样,通过索引访问数据时就会得到与全表扫描不同的答案。这种确定性设置表明在有些函数上是不能建立索引的,例如,我们无法在函数DBMS_RANDOM.RANDOM上创建索引,因为这是一个随机数生成器。函数DBMS_RANDOM.RANDOM的结果不是确定性的;给定相同的输入,我们会得到随机的输出。另一方面,第一个例子中所用的内置SQL函数UPPER则是确定性的,所有可以在列的UPPER值上创建一个索引。

既然有了函数MY_SOUNDEX,下面来看没有索引时表现如何。在此使用了前面创建的EMP表(其中有大约11.,000行):

ops$tkyte@ORA10G> set timing on

ops$tkyte@ORA10G> set autotrace on explain

ops$tkyte@ORA10G> select ename, hiredate

2 from emp

3 where my_soundex(ename) = my_soundex('Kings')

4 /

 

ENAME             HIREDATE

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

Ku$_Chunk_   11.-AUG-04

Ku$_Chunk_   11.-AUG-04

Elapsed: 00:00:01.07

 

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=32 Card=100 Bytes=1900)

11.0       TABLE ACCESS (FULL) OF 'EMP' (TABLE) (Cost=32 Card=100 Bytes=1900)

ops$tkyte@ORA10G> set autotrace off

ops$tkyte@ORA10G> set timing off

ops$tkyte@ORA10G> set serveroutput on

ops$tkyte@ORA10G> exec dbms_output.put_line( stats.cnt );

19998

PL/SQL procedure successfully completed.

可以看到这个查询花了一秒多的时间执行,而且必须执行全表扫描。函数MY_SOUNDEX被调用了几乎20,000次(根据计数器得出),每行要调用两次。

下面来看对这个函数建立索引后,速度会有怎样的提高。首先如下创建索引:

ops$tkyte@ORA10G> create index emp_soundex_idx on

2 emp( substr(my_soundex(ename),11.6) )

3 /

Index created.

在这个CREATE INDEX命令中,有意思的是在此使用了SUBSTR函数。这是因为,我们在对一个返回串的函数建索引。如果对一个返回数字或日期的函数建索引,就没有必须使用这个SUBSTR。如果用户编写的函数返回一个串,之所以要对这样一个函数使用SUBSTR,原因是这种函数会返回VARCHAR2(4000)类型。这就太大了,无法建立索引,索引条目必须能在块大小的3/4中放得下。如果尝试对这么大的返回值建索引,就会收到以下错误(在一个块大小为4KB的表空间中):

ops$tkyte@ORA10G> create index emp_soundex_idx on

2 emp( my_soundex(ename) ) tablespace ts4k;

emp( my_soundex(ename) ) tablespace ts4k

*

ERROR at line 2:

ORA-01450: maximum key length (3118) exceeded

这并不是说索引中确实包含那么大的键,而是说对数据库而言键可以有这么大。但是数据库“看得懂“SUBSTR。它看到SUBSTR的输入参数为16,知道最大的返回值是6个字符;因此,它允许创建索引。你很可能会遇到这种大小问题,特别是对于串联索引。以下是一个例子,在此表空间的块大小为8KB

ops$tkyte@ORA10G> create index emp_soundex_idx on

2 emp( my_soundex(ename), my_soundex(job) );

emp( my_soundex(ename), my_soundex(job) )

*

ERROR at line 2:

ORA-01450: maximum key length (6398) exceeded

在此,数据库认为最大的键为6,398,所以CREATE再一次失败。因此,如果用户编写的函数要返回一个串,要对这样一个函数建立索引,应当在CREATE INDEX语句中对返回类型有所限制。在这个例子中,由于知道MY_SOUNDEX最多返回6个字符,所以取前6个字符作为字串。

有了这个索引后,现在来测试表的性能。我们想监视索引对INSERT的影响,并观察它能怎样加快SELECT的执行速度。在没有索引的测试用例中,我们的查询用了1秒多的时间,如果在插入期间运行SQL_TRACETKPROF,会观察到:在没有索引的情况下,插入9,999条记录耗时约0.5秒:

insert into emp NO_INDEX

(empno,ename,job,mgr,hiredate,sal,comm,deptno)

select rownum empno,

        initcap(substr(object_name,11.11.)) ename,

        substr(object_type,11.9) JOB,

        rownum MGR,

        created hiredate,

        rownum SAL,

        rownum COMM,

        (mod(rownum,4)+1)*11. DEPTNO

from all_objects

where rownum < 10000

 

call    count        cpu    elapsed        disk         query      current          rows

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

Parse        1       0.03           0.06             0                 0                 0                 0

Execute    1       0.46           0.43             0       15439            948          9999

Fetch        0       0.00           0.00             0                 0                 0                 0

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

total           2       0.49           0.50             0       15439            948          9999

但是如果有索引,则需要大约11.2秒:

call    count        cpu    elapsed        disk         query      current          rows

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

Parse        1       0.03           0.04             0                 0                 0                 0

Execute    1       11.11.           11.11.             2       15650          7432          9999

Fetch        0       0.00           0.00             0                 0                 0                 0

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

total           2       11.11.           11.11.             2       15650          7432          9999

原因在于管理MY_SOUNDEX函数上的新索引会带来开销,这一方面是因为只要有索引就存在相应的性能开销(任何类型的索引都会影响插入的性能);另一方面是因为这个索引必须把一个存储过程调用9,999次。

下面测试这个查询,只需再次运行查询:

ops$tkyte@ORA10G> REM reset our counter

ops$tkyte@ORA10G> exec stats.cnt := 0

PL/SQL procedure successfully completed.

 

ops$tkyte@ORA10G> set timing on

ops$tkyte@ORA10G> set autotrace on explain

ops$tkyte@ORA10G> select ename, hiredate

2 from emp

3 where substr(my_soundex(ename),11.6) = my_soundex('Kings')

4 /

 

ENAME HIREDATE

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

Ku$_Chunk_ 11.-AUG-04

Ku$_Chunk_ 11.-AUG-04

Elapsed: 00:00:00.02

 

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2 Card=11.Bytes=11.)

11.0       TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (TABLE) (Cost=2 Card=11.Bytes=11.)

2 1           INDEX (RANGE SCAN) OF 'EMP_SOUNDEX_IDX' (INDEX) (Cost=11.Card=35)

 

ops$tkyte@ORA10G> set autotrace off

ops$tkyte@ORA10G> set timing off

ops$tkyte@ORA10G> set serveroutput on

ops$tkyte@ORA10G> exec dbms_output.put_line( stats.cnt );

2

 

PL/SQL procedure successfully completed.

如果对这两个例子做个比较(无索引和有索引),会发现插入受到的影响是:其运行时间是原来的两倍还多。不过,选择操作则不同,原来需要1秒多的时间,现在几乎是“立即”完成。这里的要点是:

q         有索引时,插入9,999条记录需要大约两倍多的时间。对用户编程的函数建立索引绝对会影响插入(和一些更新)的性能。当然,你应该意识到任何索引都会影响性能。例如,我做了一个简单的测试(没有MY_SOUNDEX函数),其中只是对ENAME列本身加索引。这就导致INSERT花了大约1秒的时间执行,因此整个开销并不能全部归咎于PL/SQL函数。由于大多数应用都只是插入和更新单个的条目,而且插入每一行只会花不到11.11.,000秒的时间,所以在一个经典的应用中,你可能注意不到这种开销。由于我们一次只插入一行,代价就只是在列上执行一次函数,而不是像查询数据时那样可能执行数千次。

q         尽管插入的运行速度慢了两倍多,但查询运行的速度却快了几倍。它只是把MY_SOUNDEX函数计算了几次,而不是几乎20,000次。这里有索引和无索引时查询性能的差异相当显著。另外,表越大,全面扫描查询执行的时间就会越来越长。基于索引的查询则不同,随着表的增大,基于索引的查询总是有几乎相同的执行性能。

q         在我们的查询中必须使用SUBSTR。这好像不太好,不如只是写WHERE MY_SOUNDEX(ename)=MY_SOUNDEX(‘King’)那么直接,但是这个问题可以很容易地得到解决,稍后就会看到。

因此,插入会受到影响,但是查询运行得快得多。尽管插入/更新性能稍有下降,但是回报是丰厚的。另外,如果从不更新MY_SOUNDEX函数调用中涉及到的列,更新就根本没有开销(仅当修改了ENAME列而且其值确实有改变时,才会调用MY_SOUNDEX)。

现在来看如何让查询不使用SUBSTR函数调用。使用SUBSTR调用可能很容易出错,最终用户必须知道要从第1个字符起取6个字符作为子串(SUBSTR)。如果使用的子串大小不同,就不会使用这个索引。另外,我们可能希望在服务器中控制要索引的字节数。因此我们可以重新实现MY_SOUNDEX函数,如果愿意还可以索引7个字节而不是6个。利用一个视图就能非常简单地隐藏SUBSTR,如下所示:

ops$tkyte@ORA10G> create or replace view emp_v

2 as

3 select ename, substr(my_soundex(ename),11.6) ename_soundex, hiredate

4 from emp

5 /

View created.

 

ops$tkyte@ORA10G> exec stats.cnt := 0;

PL/SQL procedure successfully completed.

 

ops$tkyte@ORA10G> set timing on

ops$tkyte@ORA10G> select ename, hiredate

2 from emp_v

3 where ename_soundex = my_soundex('Kings')

4 /

ENAME          HIREDATE

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

Ku$_Chunk_   11.-AUG-04

Ku$_Chunk_   11.-AUG-04

 

Elapsed: 00:00:00.03

ops$tkyte@ORA10G> set timing off

ops$tkyte@ORA10G> exec dbms_output.put_line( stats.cnt )

2

PL/SQL procedure successfully completed.

可以看到这个查询计划与对基表的查询计划是一样的。这里所做的只是将SUBSTR(F(X)),11.6)隐藏在视图本身中。优化器会识别出这个虚拟列实际上是加了索引的列,并采取“正确”的行动。我们能看到同样的性能提升和同样的查询计划。使用这个视图与使用基表是一样的,甚至还更好一些,因为它隐藏了复杂性,并允许我们以后改变SUBSTR的大小。

11.4.3             只对部分行建立索引

基于函数的索引除了对使用内置函数(如UPPERLOWER等)的查询显然有帮助之外,还可以用来有选择地只是对表中的某些行建立索引。稍后会讨论,B*树索引对于完成为NULL的键没有相应的条目。也就是说,如果在表T上有一个索引I

Create index I on t(a,b);

而且行中AB都为NULL,索引结构中就没有相应的条目。如果只对表中的某些行建立索引,这就能用得上。

考虑有一个很大的表,其中有一个NOT NULL列,名为PROCESSED_FLAG,它有两个可取值:YN,默认值为N。增加新行时,这个值为N,指示这一行未得到处理,等到处理了这一行后,则会将其更新为Y来指示已处理。我们可能想对这个列建立索引,从而能快速地获取值为N的记录,但是这里有数百万行,而且几乎所有行的值都为Y。所得到的B*树索引将会很大,如果我们把值从N更新为Y,维护这样一个大索引的开销也相当高。这个表听起来很适合采用位图索引(毕竟基数很低!),但这是一个事务性系统,可能有很多人在同时插入记录(新记录的“是否处理”列设置为N),前面讨论过,位图索引不适用于并发修改。如果考虑到这个表中会不断地将N更新为Y,那位图就更不合适了,根本不应考虑,因为这个过程会完全串行化。

所以,我们真正想做的是,只对感兴趣的记录建立索引(即该列值为N的记录)。我们会介绍如何利用基于函数的索引来做到这一点,但是在此之前,先来看如果只是一个常规索引会发生什么。使用本书最前面“环境设置”一节中描述的标准BIG_TABLE脚本,下面更新TEMPORARY列,在此将Y变成N,以及N变成Y

ops$tkyte@ORA10G> update big_table set temporary = decode(temporary,'N','Y','N');

1000000 rows updated.

现在检查YN地比例:

ops$tkyte@ORA10G> select temporary, cnt,

2     round( (ratio_to_report(cnt) over ()) * 100, 2 ) rtr

3 from (

4         select temporary, count(*) cnt

5         from big_table

6         group by temporary

7 )

8 /

 

T            CNT               RTR

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

N          1779                 .11.

Y      998221             99.82

可以看到,在表的11.000,000条记录中,只有0.2%的数据应当加索引。如果在TEMPORARY列上使用传统索引(相对于这个例子中PROCESSED_FLAG列的角色),会发现这个索引有11.000,000个条目,占用了超过14MB的空间,其高度为3

ops$tkyte@ORA10G> create index processed_flag_idx

2 on big_table(temporary);

Index created.

 

ops$tkyte@ORA10G> analyze index processed_flag_idx

2 validate structure;

Index analyzed.

 

ops$tkyte@ORA10G> select name, btree_space, lf_rows, height

2 from index_stats;

NAME                                    BTREE_SPACE    LF_ROWS    HEIGHT

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

PROCESSED_FLAG_IDX           14528892       1000000                 3

通过这个索引获取任何数据都会带来3I/O才能达到叶子块。这个索引不仅很“宽”,还很“高”。要得到第一个未处理的记录,必须至少执行4I/O(其中3个是对索引的I/O,另外一个是对表的I/O)。

怎么改变这种情况呢?我们要让索引更小一些,而且要更易维护(更新期间的运行时开销更少)。采用基于函数的索引,我们可以编写一个函数,如果不想对某个给定行加索引,则这个函数就返回NULL;而对想加索引的行则返回一个非NULL值。例如,由于我们只对列值为N的记录感兴趣,所以只对这些记录加索引:

ops$tkyte@ORA10G> drop index processed_flag_idx;

Index dropped.

 

ops$tkyte@ORA10G> create index processed_flag_idx

2 on big_table( case temporary when 'N' then 'N' end );

Index created.

 

ops$tkyte@ORA10G> analyze index processed_flag_idx

2 validate structure;

Index analyzed.

 

ops$tkyte@ORA10G> select name, btree_space, lf_rows, height

2 from index_stats;

NAME                                    BTREE_SPACE    LF_ROWS    HEIGHT

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

PROCESSED_FLAG_IDX                  40012              1779                 2

这就有很大不同,这个索引只有大约40KB,而不是11..5MB。高度也有所降低。与前面那个更高的索引相比,使用这个索引能少执行一个I/O

11.4.4             实现有选择的惟一性

要利用基于函数的索引,还有一个有用的技术,这就是使用这种索引来保证某种复杂的约束。例如,假设有一个带版本信息的表,如项目表。项目有两种状态:要么为ACTIVE,要么为INACTIVE。需要保证以下规则:“活动的项目必须有一个惟一名;而不活动的项目无此要求。”也就是说,只有一个活动的“项目X”,但是如果你愿意,可以有多个名为X的不活动项目。

开发人员了解到这个需求时,第一反应往往是:“我们只需运行一个查询来查看是否有活动项目X,如果没有,就可以创建一个活动项目X。”如果你读过第7章(介绍并发控制和多版本的内容),就会知道,这种简单的实现在多用户环境中是不可行的。如果两个人想同时创建一个新的活动项目X,他们都会成功。我们需要将项目X的创建串行化,但是对此惟一的做法是锁住这个项目表(这样做并发性就不太好了),或者使用一个基于函数的索引,让数据库为我们做这个工作。

由于可以在函数上创建索引,而且B*树索引中对于完全为NULL的行没有相应的条目,另外我们可以创建一个UNIQUE索引,基于这几点,可以很容易做到:

Create unique index active_projects_must_be_unique

On projects ( case when status = 'ACTIVE' then name end );

这就行了。状态(status)列是ACTIVE时,NAME列将建立惟一的索引。如果试图创建同名的活动项目,就会被检测到,而且这根本不会影响对这个表的并发访问。

11.4.5             关于CASE的警告

某些Oracle版本中有一个bug,其中基于函数的索引中引用的函数会以某种方式被重写,以至于索引无法被透明地使用。例如,前面的CASE语句

Case when temporary = 'N' then 'N' end

会悄悄地重写为以下更高效的语句:

CASE "TEMPORARY" WHEN 'N' THEN 'N' END

但是这个函数与我们创建的那个函数不再匹配,所以查询无法使用此函数。如果在11..11.0.3中执行这个简单的测试用例,然后再在11..11.0.4(该版本修正了这个bug)中执行它,结果如下(在11..11.0.3中):

ops$tkyte@ORA10GR1> create table t ( x int );

Table created.

 

ops$tkyte@ORA10GR1> create index t_idx on

2 t( case when x = 42 then 11.end );

Index created.

 

ops$tkyte@ORA10GR1> set autotrace traceonly explain

 

ops$tkyte@ORA10GR1> select /*+ index( t t_idx ) */ *

2 from t

3 where (case when x = 42 then 11.end ) = 1;

 

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2 Card=11.Bytes=11.)

11.0       TABLE ACCESS (FULL) OF 'T' (TABLE) (Cost=2 Card=11.Bytes=11.)

看上去,基于函数的索引不仅不会工作,而且不可用。但是这个FBI(基于函数的索引)其实是可用的,只不过这里底层函数被重写了,我们可以查看视图USER_IND_EXPRESSIONS来看看Oracle是如何重写它的,从而验证这一点:

ops$tkyte@ORA10GR1> select column_expression

2 from user_ind_expressions

3 where index_name = 'T_IDX';

COLUMN_EXPRESSION

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

CASE "X" WHEN 42 THEN 11.END

Oracle11..11.0.4中,基于函数的索引中也会发生重写,但是索引会使用重写后的函数:

ops$tkyte@ORA10G> set autotrace traceonly explain

ops$tkyte@ORA10G> select /*+ index( t t_idx ) */ *

2 from t

3 where (case when x = 42 then 11.end ) = 1;

 

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=11.Card=11.Bytes=11.)

11.0       TABLE ACCESS (BY INDEX ROWID) OF 'T' (TABLE) (Cost=11.Card=11.Bytes=11.)

2 1           INDEX (RANGE SCAN) OF 'T_IDX' (INDEX) (Cost=11.Card=1)

这是因为数据库选择不仅重写了CREATE INDEX中的函数,还重写了查询本身使用的函数,因此二者是匹配的。

在以前的版本中,对此的解决办法有以下几种:

q         使用DECODE而不是CASE,因为DECODE不会被重写,即所谓的“所见即所得”。

q         使用最先搜索到的CASE语法(预计到可能会发生的优化)。

但是,倘若优化器没有使用你的基于函数的索引,而且你对此无法做出解释,不知道为什么没有使用你的函数,在这种情况下,就可以检查USER_IND_EXPRESSIONS视图,验证使用的函数是否正确。

11.4.6             关于ORA-01743的警告

对于基于函数的索引,我注意到这样一个奇怪的现像,如果你要在内置函数TO_DATE上创建一个索引,某些情况下并不能成功创建,例如:

ops$tkyte@ORA10GR1> create table t ( year varchar2(4) );

Table created.

 

ops$tkyte@ORA10GR1> create index t_idx on t( to_date(year,'YYYY') );

create index t_idx on t( to_date(year,'YYYY') )

*

ERROR at line 1:

ORA-01743: only pure functions can be indexed

这看上去很奇怪,因为有时使用TO_DATE创建索引确实是可以的,例如:

ops$tkyte@ORA10GR1> create index t_idx on t( to_date('01'||year,'MMYYYY') );

Index created.

相关的错误信息也很模糊,让人摸不着头脑:

ops$tkyte@ORA10GR1> !oerr ora 1743

01743, 00000, "only pure functions can be indexed"

// *Cause: The indexed function uses SYSDATE or the user environment.

// *Action: PL/SQL functions must be pure (RNDS, RNPS, WNDS, WNPS). SQL

// expressions must not use SYSDATE, USER, USERENV(), or anything

// else dependent on the session state. NLS-dependent functions

// are OK.

我们并没有使用SYSDATE;也没有使用“用户环境”(难道我们用了吗?)。这里并没有使用PL/SQL函数,而且没有涉及任何与会话状态有关的方面。问题只是在于我们使用的格式:YYYY。给定完全相同的输入,这种格式可能返回不同的答案,这取决于调用的函数时输入的月份。例如,对于5月的每个时间:

ops$tkyte@ORA10GR1> select to_char( to_date('2005','YYYY'),

2                                                                'DD-Mon-YYYY HH24:MI:SS' )

3 from dual;

TO_CHAR(TO_DATE('200

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

01-May-2005 00:00:00

YYYY格式会返回51,在6月它会返回61,以此类推。这就说明,如果用到YYYYTO_DATE就不是确定性的!这这是无法创建索引的原因:只有在创建一行(或插入/更新一行)的多月它能正确工作。所以,这个错误确实归根于用户环境,其中包含当前日期本身。

要在一行基于函数的索引中使用TO_DATE,必须使用一种无歧义的确定性日期格式,而不论当前是哪一天。

11.4.7             基于函数的索引小结

基于函数的索引很容易使用和实现,他们能提供立即值。可以用基于函数的索引来加快现有应用的速度,而不用修改应用中的任何逻辑或查询。通过使用基于函数的索引,可以观察到性能会呈数量级地增长。使用这种索引能提前计算出复杂的值,而无需使用触发器。另外,如果在基于函数的索引中物化表达式,优化器就能更准确度估计出选择性。可以使用基于函数的索引有选择地只对感兴趣的几行建立索引(如前面关于PROCESSED_FLAG的例子所示)。实际上,使用这种就是可以对WHERE子句加索引。最后,我们研究了如何使用基于函数的索引来实现某种完整性约束:有选择的惟一性(例如,“每个条件成立时字段XYZ必须惟一”)。

基于函数的索引会影响插入和更新的性能。不论这一点对你是否重要,都必须有所考虑。如果你总是插入数据,而不经常查询,基于函数的索引可能对你并不适用。另一方面,要记住,一般插入时都是一次插入一行,查询却会完成数千次。所以插入方面的性能下降(最终用户可能根本注意不到)能换来查询速度数千倍的提高。一般来说,在这种情况下利远大于弊。

11.5   应用域索引

应用域索引(application domain index)即Oracle所谓的可扩展索引(extensible indexing)。利用应用域索引,你可以创建自己的索引结构,使之像Oracle提供的索引一样工作。有人使用你的索引类型发出一个CREATE INDEX语句时,Oracle会运行你的代码来生成这个索引。如果有人分析索引来计算统计信息,Oracle会执行你的代码来生成统计信息(采用你要求的存储格式)。Oracle解析查询并开发查询计划时,如果查询计划中可能使用你的索引,Oracle会问你:这个函数的计算不同的计划时会有怎样的开销。简单地说,利用应用域索引,你能实现数据库中原本没有的一个新的索引类型。例如,如果你开发一个软件来分析数据库中存储的图像,而且生成了关于图像的信息(如图像中的颜色),就可以创建你自己的图像(image)索引。向数据库中增加图象时,会调用你的代码,从图像中抽取颜色,并将其存储在某个地方(你想存储图像索引的任何地方)。查询时,用户请求所有“蓝色图像”时,Oracle就会在合适的时候从索引提供答案。

对此最好的例子是Oracle自己的文本索引(text index)。这个索引用于对大量的文本项提供关键字搜索。可以如下创建一个简单的文本索引:

ops$tkyte@ORA10G> create index myindex on mytable(docs)

2 indextype is ctxsys.context

3 /

Index created.

这个索引的创建者向SQL语言中引入了一些文本运算符,接下来使用这些文本运算符:

select * from mytable where contains( docs, 'some words' ) > 0;

它甚至能对如下的命令做出响应:

ops$tkyte@ORA10GR1> begin

2 dbms_stats.gather_index_stats( user, 'MYINDEX' );

3 end;

4 /

PL/SQL procedure successfully completed.

它会与优化器合作,在运行时确定使用文本索引(而不是其他某个索引或前面扫描)的相对开销。有意思的是,任何人(包括你和我)都可以开发这样一个索引。文本索引的实现无需你了解“内部核心知识”。这是使用专用的API完成的,这些API有文档说明而且已经公开提供。Oracle数据库内核并不关心文本索引如果存储(对于创建的每个索引,API会把它存储在多个物理数据库表中)。Oracle也不知道插入新行时会做怎样的处理。Oracle文本实际上是建立在数据库之上的一个应用,但采用了一种完全集成的方式。对于你和我来说,这看上去就像是如何其他Oracle数据库内核函数一样,但事实上它并不是内核函数。

我个人认为,没有必要去构建一个标新立异的索引结构类型,在我看来,这种特定的特性大多由第三方解决方案提供者使用(他们有一些创新性的索引技术)。

我认为,应用域索引最有意思的一点是:利用应用域索引,这就允许其他人提供新的索引技术,而我可以在自己的应用中使用这些技术。大多数人从来都没有用过这种特定的API来构建新的索引类型,但是我们大多都用到过某种非内置的新索引类型。我参与的几乎每一个应用都有一些与之相关的文本(text)、待处理的XML或者要存储和分类的图像(image)。这些功能通过一个interMedia功能集(利用了应用域索引特性来实现)就能提供。随着时间的推移,可用的索引类型越来越多。我们将在下一章更深入地分析文本索引。

11.6   关于索引的常见问题和神话

在本书的引言中曾经说过,我回答过大量关于Oracle的问题。我就是Oracle Magazine上“Ask Tom”专栏和http://asktom.oracle.com上的Tom,在这个专栏和网站上我一直在回答大家提出的关于Oracle数据库和工具的问题。根据我的经验,其中关于索引的问题最多。这一节中,我将回答问得最多的一些问题。有些答案就像是常识一样,很直接;但是有些答案可能会让你很诧异。可以这么说,关于索引存在的许多神话和误解。

11.6.1             视图能使用索引吗?

与这个问题相关的另一个问题是:“能对视图加索引吗?”视图实际上就是一个存储查询(stored query)。Oracle会把查询中访问视图的有关文本代之以视图定义本身。视图只是为了方便最终用户或程序员,优化器还是会对基表使用查询。使用视图时,完全可以考虑使用为基表编写的查询中所能使用的所有索引。“对视图建立索引”实际上就是对基本建立索引。

11.6.2             Null和索引能协作吗?

B*树索引(除了聚簇B*树索引这个特例之外)不会存储完全为null的条目,而位图好聚簇索引则不同。这个副作用可能会带来一些混淆,但是如果你理解了不存储完全为null的键是什么含义,就能很好地利用这一点。

要看到不存储null值所带来的影响,请考虑下面这个例子:

ops$tkyte@ORA10GR1> create table t ( x int, y int );

Table created.

 

ops$tkyte@ORA10GR1> create unique index t_idx on t(x,y);

Index created.

 

ops$tkyte@ORA10GR1> insert into t values ( 1, 1 );

11.row created.

 

ops$tkyte@ORA10GR1> insert into t values ( 1, NULL );

11.row created.

 

ops$tkyte@ORA10GR1> insert into t values ( NULL, 1 );

11.row created.

 

ops$tkyte@ORA10GR1> insert into t values ( NULL, NULL );

11.row created.

 

ops$tkyte@ORA10GR1> analyze index t_idx validate structure;

Index analyzed.

 

ops$tkyte@ORA10GR1> select name, lf_rows from index_stats;

NAME                                      LF_ROWS

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

T_IDX                                                       3

这个表有4行,而索引只有3行。前三行(索引键元素中至少有一个不为null)都在索引中。最后一行的索引键是(NULL,NULL,所以这一行不在索引中。倘若索引是一个惟一索引(如上所示),这就是可能产生混淆的一种情况。考虑以下3INSERT语句的作用:

ops$tkyte@ORA10GR1> insert into t values ( NULL, NULL );

11.row created.

 

ops$tkyte@ORA10GR1> insert into t values ( NULL, 1 );

insert into t values ( NULL, 1 )

*

ERROR at line 1:

ORA-00001: unique constraint (OPS$TKYTE.T_IDX) violated

 

ops$tkyte@ORA10GR1> insert into t values ( 1, NULL );

insert into t values ( 1, NULL )

*

ERROR at line 1:

ORA-00001: unique constraint (OPS$TKYTE.T_IDX) violated

这里并不认为新的(NULL,NULL)行与原来的(NULL,NULL)行相同:

ops$tkyte@ORA10GR1> select x, y, count(*)

2 from t

3 group by x,y

4 having count(*) > 1;

X              Y                  COUNT(*)

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

                                                        2

看上去好像不可能的,如果考虑到所有null条目,这就说明我们的惟一键并不惟一。事实上,在Oracle中,考虑惟一性时(NULL,NULL)与(NULL,NULL)并不相同,这是SQL标准要求的。不过对于聚集来说(NULL,NULL)和(NULL,NULL)则认为是相同的。两个(NULL,NULL)在比较时并不相同,但是对GROUP BY 子句来说却是一样的。所以应当考虑到:每个惟一约束应该至少有一个确实惟一的NOT NULL列。

关于索引和null值还会提出这样一个疑问是:“为什么我的查询不使用索引?”下面是一个有问题的查询:

select * from T where x is null;

这个查询无法使用我们刚才创建的索引,(NULL,NULL)行并不在索引中,因此使用索引的话实际上会返回错误的答案。只有当索引键中至少有一个列定义为NOT NULL时查询才会使用索引。例如,以下显示了Oracle会对X IS NULL谓词使用索引(如果索引的索引键最前面是X列,而且索引中其他列中至少有一列是NOT NULL):

ops$tkyte@ORA10GR1> create table t ( x int, y int NOT NULL );

Table created.

 

ops$tkyte@ORA10GR1> create unique index t_idx on t(x,y);

Index created.

 

ops$tkyte@ORA10GR1> insert into t values ( 1, 1 );

11.row created.

 

ops$tkyte@ORA10GR1> insert into t values ( NULL, 1 );

11.row created.

 

ops$tkyte@ORA10GR1> begin

2     dbms_stats.gather_table_stats(user,'T');

3 end;

4 /

PL/SQL procedure successfully completed.

再来查询这个表,会发现:

ops$tkyte@ORA10GR1> set autotrace on

ops$tkyte@ORA10GR1> select * from t where x is null;

X                           Y

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

                               1

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=11.Card=11.Bytes=5)

11.0       INDEX (RANGE SCAN) OF 'T_IDX' (INDEX (UNIQUE)) (Cost=11.Card=11.Bytes=5)

前面我说过,B*树索引中不存储完全为null的条目,而且你可以充分利用这一点,以上就展示了应当如何加以利用。假设你有一个表,其中每一列只有两个可取值。这些值分布得很不均匀,例如,90%以上的行(多数行)都取某个值,而另外不到11.%的行(少数行)取另外一个值。可以有效地对这个列建立索引,来快速访问那些少数行。如果你想使用一个索引访问少数行,同时又想通过全面扫描来访问多数行,另外还想节省空间,这个特性就很有用。解决方案是:对多数行使用null,而对少数行使用你希望的任何值;或者如前所示,使用一个基于函数的索引,只索引函数的非null返回值。

既然知道了B*树如何处理null值,所以可以充分利用这一点,并预防在全都允许有null值的列上建立惟一约束(当心这种情况下可能有多个全null的行)。

11.6.3             外键是否应该加索引?

外键是否应该加索引,这个问题经常被问到。我们在第6章讨论死锁时谈到过这个话题。在第6章中,我指出,外键未加索引是我所遇到的导致死锁的最主要的原因;这是因为,无论是更新父表主键,或者删除一个父记录,都会在子表中加一个表锁(在这条语句完成前,不允许对子表做任何修改)。这就会不必要地锁定更多的行,而影响并发性。人们在使用能自动生成SQL来修改表的某个工具时,就经常遇到这种问题。这样的工具会生成一个更新语句,它将更新表中的每一列,而不论这个值是否被UPDATE语句修改。这就会导致更新主键(即使主键值其实从未改变过)。例如,Oracle Forms就会默认地这样做,除非你告诉它只把修改过的列发送给数据库。除了可能遇到表锁问题之外,在以下情况下,外键未加索引也表现得很糟糕:

q         如果有一个ON DELETE CASCADE,而且没有对子表建索引。例如,EMPDEPT的子表。DELETE FROM DEPT WHERE DEPTNO = 11.会级联至EMP。如果EMP中的DEPTNO没有加索引,就会导致对EMP执行一个全表扫描。这种完全扫描可能是不必要的,而且如果从父表删除了多行,对于删除的每一个父行,都会把子表扫描一次。

q         从父表查询子表时。还是考虑EMP/DEPT的例子。在DEPTNO上下文查询EMP表相当常见。如果频繁地执行以下查询来生成一个报告或某个结果:

select *

from dept, emp

where emp.deptno = dept.deptno

and dept.dname = :X;

你会发现,如果没有索引会使查询减慢。由于同样的原因,我在第11.章曾建议对嵌套表中的NESTED_COLUMN_ID加索引。嵌套表的隐藏列NESTED_COLUMN_ID实际上就是一个外键。

那么,什么时候不需要对外键加索引呢?一般来说,如果满足以下条件则可如此:

q         未删除父表中的行。

q         不论是有意还是无意(如通过一个工具),总之未更新父表的惟一/主键值。

q         不论从父表联结到子表,或者更一般地讲,外键列不支持子表的一个重要的访问途径,而且你在谓词中没有使用这些外键列从子表中选择数据(如DEPTEMP)。

如果满足上述所有3个条件,就完全可以不加索引,也就是说,对外键加索引是不必要的,还会减慢子表上DML操作的速度。如果满足了其中某个条件,就要当心不加索引的后果。

另外说一句,如果你认为某个子表会由于外键为加索引而被锁住,而且希望证明这一点(或者一般来说,你想避免这种情况),可以发出以下命令:

ALTER TABLE <child table name> DISABLE TABLE LOCK;

现在,对父表的可能导致表锁的任何UPDATEDELETE都会接收到以下错误:

ERROR at line 1:

ORA-00069: cannot acquire lock -- table locks disabled for <child table name>

这有助于跟踪到有问题的代码段,你以为它没有做某件事(比如,你认为并没有对父表的主键执行UPDATEDELETE),但实际上事与愿违,通过以上命令,最终用户就会立即向你反馈这个错误。

11.6.4             为什么没有使用我的索引?

对此有很多可能的原因。在这一节中,我们会查看其中一些最常见的原因。

1.      情况1

我们在使用一个B*树索引,而且谓词中没有使用索引的最前列。如果是这种情况,可以假设有一个表T,在T(X,Y)上有一个索引。我们要做以下查询:SELECT * FROM T WHERE Y = 5。此时,优化器就不打算使用T(x,y)上的索引,因为谓词中不涉及X列。在这种情况下,倘若使用索引,可能就必须查看每一个索引条目(稍后我们会讨论一种索引跳跃式扫描,这是一种例外情况),而优化器通常更倾向于T对做一个全表扫描。但这并不完全排除使用索引。如果查询是SELECT X, Y FROM T WHERE Y = 5,优化器就会注意到,它不必全面扫描表来得到XYXY都在索引中),对索引本身做一个快速的全面扫描会更合适,因为这个索引一般比底层表小得多。还要注意,仅CBO能使用这个访问路径。

另一种情况下CBO也会使用T(x,y)上的索引,这就是索引跳跃式扫描。当且仅当索引的最前列(在上一个例子中,最前列就是Y)只有很少的几个不同值,而且优化器了解这一点,跳跃式扫描(skip scan)就能很好地发挥作用。例如,考虑(GENDER, EMPNO)上的一个索引,其中GENDER可取值有MF,而且EMPNO是惟一的。对于以下查询:

select * from t where empno = 5;

可以考虑使用T上的那个索引采用跳跃式扫描方法来满足这个查询,这说明从概念上讲这个查询会如下处理:

select * from t where GENDER='M' and empno = 5

UNION ALL

select * from t where GENDER='F' and empno = 5;

它会跳跃式地扫描索引,以为这是两个索引:一个对于值M,另一个对应值F。在查询计划中可以很容易地看出这一点。我们将建立一个表,其中有一个二值的列,并在这个列上建立索引:

ops$tkyte@ORA10GR1> create table t

2 as

3 select decode(mod(rownum,2), 0, 'M', 'F' ) gender, all_objects.*

4 from all_objects

5 /

Table created.

 

ops$tkyte@ORA10GR1> create index t_idx on t(gender,object_id)

2 /

Index created.

 

ops$tkyte@ORA10GR1> begin

2     dbms_stats.gather_table_stats

3     ( user, 'T', cascade=>true );

4 end;

5 /

PL/SQL procedure successfully completed.

做以下查询时,可以看到结果如下:

ops$tkyte@ORA10GR1> set autotrace traceonly explain

ops$tkyte@ORA10GR1> select * from t t1 where object_id = 42;

 

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=4 Card=11.Bytes=95)

11.0       TABLE ACCESS (BY INDEX ROWID) OF 'T' (TABLE) (Cost=4 Card=11.Bytes=95)

2 1           INDEX (SKIP SCAN) OF 'T_IDX' (INDEX) (Cost=3 Card=1)

INDEX SKIP SCAN步骤告诉Oracle要跳跃式扫描这个索引,查找GENDER值有改变的地方,并从那里开始向下读树,然后在所考虑的各个虚拟索引中找到OBJECT_ID = 42。如果大幅增加GENDER的可取值,如下:

ops$tkyte@ORA10GR1> update t

2 set gender = chr(mod(rownum,256));

48215 rows updated.

 

ops$tkyte@ORA10GR1> begin

2     dbms_stats.gather_table_stats

3     ( user, 'T', cascade=>true );

4 end;

5 /

PL/SQL procedure successfully completed.

我们会看到,Oracle不再认为跳跃式扫描是一个可行的计划。优化器本可以去检查256个小索引,但是它更倾向于执行一个全表扫描来找到所需要的行:

ops$tkyte@ORA10GR1> set autotrace traceonly explain

ops$tkyte@ORA10GR1> select * from t t1 where object_id = 42;

 

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=158 Card=11.Bytes=95)

11.0       TABLE ACCESS (FULL) OF 'T' (TABLE) (Cost=158 Card=11.Bytes=95)

2.      情况2

我们在使用一个SELECT COUNT(*) FROM T查询(或类似的查询),而且在表T上有一个B*树索引。不过,优化器并不是统计索引条目,而是在全面扫描这个表(尽管索引比表要小)。在这种情况下,索引可能建立在一些允许有null值的列上。由于对于索引键完全为null的行不会建立相应的索引条目,所以索引中的行数可能并不是表中的行数。这里优化器的选择是对的,如若不然,倘若它使用索引来统计行数,则可能会得到错误的答案。

3.      情况3

对于一个有索引的列,做以下查询:

select * from t where f(indexed_column) = value

却发现没有使用INDEX_COLUMN上的索引。原因是这个列上使用了函数。我们是对INDEX_COLUMN的值建立了索引,而不是对F(INDEXED_COLUMN)的值建索引。在此不能使用这个索引。如果愿意,可以另外对函数建立索引。

4.      情况4

我们已经对一个字符创建了索引。这个列只包含数值数据。如果所用以下语句来查询:

select * from t where indexed_column = 5

注意查询中的数字5是常数5(而不是一个字符串),此时就没有使用INDEX_COLUMN上的索引。这是因为,前面的查询等价于一些查询:

select * from t where to_number(indexed_column) = 5

我们对这个列隐式地应用了一个函数,如情况3所述,这就会禁止使用这个索引。通过一个小例子能很容易地看出这一点。在这个例子,我们将使用内置包DBMS_XPLAN。这个包只在Oracle9i Release 2及以上版本中可用(在Oracle9i Release 1中,使用AUTOTRACE能很容易地查看计划,但是得不到谓词信息,这只在Oracle9i Release 2及以上版本中可见):

ops$tkyte@ORA10GR1> create table t ( x char(1) constraint t_pk primary key,

2 y date );

Table created.

 

ops$tkyte@ORA10GR1> insert into t values ( '5', sysdate );

11.row created.

 

ops$tkyte@ORA10GR1> delete from plan_table;

3 rows deleted.

 

ops$tkyte@ORA10GR1> explain plan for select * from t where x = 5;

Explained.

 

ops$tkyte@ORA10GR1> select * from table(dbms_xplan.display);

 

PLAN_TABLE_OUTPUT

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

Plan hash value: 749696591

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

| Id   | Operation                           | Name | Rows   | Bytes   | Cost (%CPU)    | Time          |

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

| 0    | SELECT STATEMENT    |              | 1           | 11.        | 2 (0)                    | 00:00:01   |

|* 1   | TABLE ACCESS FULL     | T           | 1           | 11.        | 2 (0)                    | 00:00:01   |

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

Predicate Information (identified by operation id):

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

1 - filter(TO_NUMBER("X")=5)

可以看到,它会全面扫描表;另外即使我们对查询给出了以下提示:

ops$tkyte@ORA10GR1> explain plan for select /*+ INDEX(t t_pk) */ * from t

2 where x = 5;

Explained.

ops$tkyte@ORA10GR1> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT

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

Plan hash value: 3473040572

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

| Id    | Operation                                          | Name    | Rows             | Bytes             | Cost (%CPU)   | Time           |

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

| 0     | SELECT STATEMENT                       |              | 1       | 11.     | 34 (0)             | 00:00:01   |

| 1     | TABLE ACCESS BY INDEX ROWID   | T           | 1       | 11.     | 34 (0)             | 00:00:01   |

|* 2    | INDEX FULL SCAN                           | T_PK     | 1       |          | 26 (0)             | 00:00:01   |

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

Predicate Information (identified by operation id):

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

2 - filter(TO_NUMBER("X")=5)

在此使用了索引,但是并不像我们想像中那样对索引完成惟一扫描(UNIQUE SCAN),而是完成了全面扫描(FULL SCAN)。原因从最后一行输出可以看出:filter(TO_NUMBER(“X”)=5)。这里对这个数据库列应用了一个隐式函数。X中存储的字符串必须转换为一个数字,之后才能与值5进行比较。在此无法把5转换为一个串,因为我们的NLS(国家语言支持)设置会控制5转换成串时的具体形式(而这是不确定的,不同的NLS设置会有不同的控制),所以应当把串转换为数字。而这样一来(由于应用了函数),就无法使用索引来快速地查找这一行了。如果只是执行串与串的比较:

ops$tkyte@ORA10GR1> delete from plan_table;

2 rows deleted.

 

ops$tkyte@ORA10GR1> explain plan for select * from t where x = '5';

Explained.

 

ops$tkyte@ORA10GR1> select * from table(dbms_xplan.display);

 

PLAN_TABLE_OUTPUT

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

Plan hash value: 1301177541

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

| Id    | Operation                                          | Name    | Rows             | Bytes             | Cost (%CPU)   | Time           |

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

| 0     | SELECT STATEMENT                       |              | 1       | 11.     | 1 (0)               | 00:00:01   |

| 1     | TABLE ACCESS BY INDEX ROWID   | T           | 1       | 11.     | 1 (0)               | 00:00:01   |

|* 2    | INDEX UNIQUE SCAN                       | T_PK     | 1       |          | 1 (0)               | 00:00:01   |

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

Predicate Information (identified by operation id):

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

2 - access("X"='5')

不出所料,这会得到我们期望的INDEX UNIQUE SCAN,而且可以看到这里没有应用函数。一定要尽可能地避免隐式转换。苹果和橘子本来就是两样东西,苹果就和苹果比,而橘子就该和橘子比。这里还经常出现一个关于日期的问题。如果做以下查询:

-- find all records for today

select * from t where trunc(date_col) = trunc(sysdate);

而且发现这个查询没有使用DATE_COL上的索引。为了解决这个问题。可以对TRUNC(DATE_COL)建立索引,或者使用区间比较运算符来查询(也许这是更容易的做法)。下面来看对日期使用大于和小于运算符的一个例子。可以认识到以下条件:

TRUNC(DATE_COL) = TRUNC(SYSDATE)

与下面的条件是一样的:

select *

from t

where date_col >= trunc(sysdate)

and date_col < trunc(sysdate+1)

这就把所有函数都移动等式的右边,这样我们就能使用DATE_COL上的索引了(而且与WHERE TRUNC(DATE_COL)=TRUNC(SYSDATE)的效果完全一样)。

如果可能的话,倘若谓词中有函数,尽量不要对数据库列应用这些函数。这样做不仅可以使用更多的索引,还能减少处理数据库所需的工作。在上一种情况中,使用以上条件时:

where date_col >= trunc(sysdate)

and date_col < trunc(sysdate+1)

查询只会计算一次TRUNC值,然后就能使用索引来查找满足条件的值。使用TRUNC(DATE_COL) = TRUNC(SYSDATE)时,TRUNC(DATE_COL)则必须对整个表(而不是索引)中的每一行计算一次。

5.      情况5

此时如果用了索引,实际上反而会更慢。这种情况我见得太多了,人们想当然认为,索引总是会使查询更快。所以,他们会建立一个小表,再执行分析,却发现优化器并没有使用索引。在这种情况下,优化器的做法绝对是英明的。Oracle(对CBO而言)只会在合理地时候才使用索引。考虑下面的例子:

ops$tkyte@ORA10GR1> create table t

2 ( x, y , primary key (x) )

3 as

4 select rownum x, object_name

5 from all_objects

6 /

Table created.

 

ops$tkyte@ORA10GR1> begin

2 dbms_stats.gather_table_stats

3 ( user, 'T', cascade=>true );

4 end;

5 /

PL/SQL procedure successfully completed.

如果运行一个查询,它只需要表中相对较少的数据,如下:

ops$tkyte@ORA10GR1> set autotrace on explain

ops$tkyte@ORA10GR1> select count(y) from t where x < 50;

  COUNT(Y)

        ----------

                 49

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=3 Card=11.Bytes=28)

11.0       SORT (AGGREGATE)

2 1           TABLE ACCESS (BY INDEX ROWID) OF 'T' (TABLE) (Cost=3 Card=41 Bytes=1148)

3 2               INDEX (RANGE SCAN) OF 'SYS_C009167' (INDEX (UNIQUE)) (Cost=2 Card=41)

此时,优化器会很乐意地使用索引;不过,我们发现,如果估计通过索引获取的行数超过了一个阀值(取决于不同的优化器设计、物理统计等,这个阀值可能有所变化),就会观察到优化器将开始一个全部扫描:

ops$tkyte@ORA10GR1> select count(y) from t where x < 15000;

  COUNT(Y)

        ----------

          14999

 

Execution Plan

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

0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=57 Card=11.Bytes=28)

11.0       SORT (AGGREGATE)

2 1           TABLE ACCESS (FULL) OF 'T' (TABLE) (Cost=57 Card=14994 Bytes=419832)

这个例子显示出优化器不一定会使用索引,而且实际上,它会做出正确的选择:采用跳跃式索引。对查询调优时,如果发现你认为本该使用的某个索引实际上并没有用到,就不要冒然强制使用这个索引,而应该先做个测试,并证明使用这个索引后确实会加快速度(通过耗用的时间和I/O次数来判断),然后再考虑让CBO“就范”(强制它使用这个索引)。总得先给出理由吧。

6.      情况6

有一段时间没有分析表了。这些表起先很小,但等到查看时,它们已经增长得非常大。现在索引就还有意义(尽管原先并非如此)。如果此时分析这个表,就会使用索引。

如果没有正确的统计信息,CBO将无法做出正确的决定。

7.      索引情况小结

根据我的经验,这6种情况就是不使用索引的主要原因。归根结底,原因通常就是“不能使用索引,使用索引会返回不正确的结果“,或者”不应该使用,如果使用了索引,性能会变得很糟糕“。

11.6.5             神话:索引中从不重用空间

这是我要彻底揭穿的一个神话:在索引中确实会重用空间。这个神话是这样说的:假设有一个表T,其中有一个列X。在某个时间点上,你在表中放了一个值X=5。后来把它删除了。据这个神话称:X=5所用的空间不会被重用,除非以后你再把X=5放回索引中。按这个神话的说法,一旦使用了某个索引槽,它就永远只能被同一个值重用。从这个神话出发还有一个推论,认为空闲空间绝对不会返回给索引结构,而且块永远不会被重用。同样,事实并非如此。

很容易证明这个神话的第一部分是错误的。我们只需如下创建一个表:

ops$tkyte@ORA10GR1> create table t ( x int, constraint t_pk primary key(x) );

Table created.

 

ops$tkyte@ORA10GR1> insert into t values (1);

11.row created.

 

ops$tkyte@ORA10GR1> insert into t values (2);

11.row created.

 

ops$tkyte@ORA10GR1> insert into t values (9999999999);

11.row created.

 

ops$tkyte@ORA10GR1> analyze index t_pk validate structure;

Index analyzed.

 

ops$tkyte@ORA10GR1> select lf_blks, br_blks, btree_space

2 from index_stats;

 

LF_BLKS     BR_BLKS    BTREE_SPACE

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

                1                     0                        7996

因此,根据这个神话所述,如果我从T中删除了X=2的行,这个空间就不会得到重用,除非我再次插入数字2。当前,这个索引使用了一个叶子块空间。如果索引键条目删除后绝对不会重用,只要我不断地插入和删除,而且从不重用任何值,那么这个索引就应该疯狂地增长。我们来看看实际是怎样的:

ops$tkyte@ORA10GR1> begin

2     for i in 2 .. 999999

3     loop

4         delete from t where x = i;

5         commit;

6         insert into t values (i+1);

7         commit;

8     end loop;

9 end;

11. /

ops$tkyte@ORA10GR1> analyze index t_pk validate structure;

Index analyzed.

 

ops$tkyte@ORA10GR1> select lf_blks, br_blks, btree_space

2 from index_stats;

 

LF_BLKS     BR_BLKS    BTREE_SPACE

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

                1                     0                        7996

由此可以看出,索引中的空间确实得到了重用。不过,就像大多数神话一样,这里也有那么一点真实的地方。真实性在于,初始数字2(介于19.999.999.999之间)所用的空间会永远保留在这个索引块上。索引不会自行“合并“。这说明,如果我用值1500,000加载一个表,然后隔行删除表记录(删除所有偶数行),那么这个索引中那一列上就会有250,000个”洞“。只有当我重新插入数据,而且这个数据能在有洞的块中放下时,这些空间才会得到重用。Oracle并不打算“收缩”或压缩索引,不过这可以通过ALTER INDEX REBUILDCOALESCE命令强制完成。另一方面,如果我用值1500,000加载一个表,然后从表中删除值小于或等于250,000的每一行,就会发现从索引中清除的块将放回到索引的freelist中,这个空间完全可以重用。

如果你还记得,第二个神话:索引空间从不“回收”。据这个神话称:一旦使用了一个索引块,它就会一直呆在索引结构的那个位置上,而且只有当你插入数据,并放回到原来那个位置上时,这个块才会被重用。同样可以证明这是错误的。首先,需要建立一个表,其中大约有500,000行。为此,我们将使用big_table脚本。有了这个表,而且有了相应的主键索引后,我们将测量索引中有多少个叶子块,另外索引的freelist上有多少个块。要记住,对于一个索引,只有当块完全为空时才会放在freelist上,这一点与表不同。所以我们在freelist上看到的块都完全为空,可以重用。

ops$tkyte@ORA10GR1> select count(*) from big_table;

COUNT(*)

----------

500000

 

ops$tkyte@ORA10GR1> declare

2     l_freelist_blocks number;

3 begin

4     dbms_space.free_blocks

5         ( segment_owner => user,

6     segment_name => 'BIG_TABLE_PK',

7     segment_type => 'INDEX',

8     freelist_group_id => 0,

9     free_blks => l_freelist_blocks );

11. dbms_output.put_line( 'blocks on freelist = ' || l_freelist_blocks );

11. end;

11. /

blocks on freelist = 0

PL/SQL procedure successfully completed.

ops$tkyte@ORA10GR1> select leaf_blocks from user_indexes

2 where index_name = 'BIG_TABLE_PK';

 

LEAF_BLOCKS

              -----------

                   1043

执行这个批量删除之前,freelist上没有块,而在索引的“叶子”层上有11.043给块,这些叶子块中包含着数据。下面,我们将执行删除,并再次测量空间的利用情况:

ops$tkyte@ORA10GR1> delete from big_table where id <= 250000;

250000 rows deleted.

 

ops$tkyte@ORA10GR1> commit;

Commit complete.

 

ops$tkyte@ORA10GR1> declare

2     l_freelist_blocks number;

3 begin

4     dbms_space.free_blocks

5         ( segment_owner => user,

6         segment_name => 'BIG_TABLE_PK',

7         segment_type => 'INDEX',

8         freelist_group_id => 0,

9         free_blks => l_freelist_blocks );

11.     dbms_output.put_line( 'blocks on freelist = ' || l_freelist_blocks );

11.     dbms_stats.gather_index_stats

11.     ( user, 'BIG_TABLE_PK' );

11. end;

11. /

blocks on freelist = 520

PL/SQL procedure successfully completed.

ops$tkyte@ORA10GR1> select leaf_blocks from user_indexes

2 where index_name = 'BIG_TABLE_PK';

 

LEAF_BLOCKS

              -----------

                     523

可以看到,现在,索引中一半以上的块都在freelist上(520个块),而且现在只有523个叶子块。如果将523520相加,又得到了原来的11.043。这说明freelist上的这些块完全为空的,而且可以重用(索引freelist上的块必须为空,这与堆组织表的freelist上的块不同)。

以上例子强调了两点:

q         一旦插入了可以重用空间的行,索引块上的空间就会立即重用。

q         索引块为空时,会从索引结构中取出它,并在以后重用。这可能是最早出现这个神话的根源:与表不同,在索引结构中,不能清楚地看出一个块有没有“空闲空间”。在表中,可以看到freelis上的块,即使其中包含有数据。而在索引中,只能在freelist上看到完全为空的块;至少有一个索引条目(但其余都是空闲空间)的块就无法清楚地看到。

11.6.6             神话:最有差别的元素应该在最前面

这看上去像是一个常识。对于一个有100,000行的表,如果要在C1C2列上创建一个索引,你发现C1100,000个不同的值,而C225,000个不同的值,你可能想在T(C1, C2)上创建索引。这说明,C1应该在前面,这是“常识性”的方法。事实上,在比较数据向量时(假设C1C2是向量),把哪一个放在前面都关系不大。考虑以下例子。我们将基于ALL_OBJECTS创建一个表,并基于OWNEROBJECT_TYPEOBJECT_NAME列创建一个索引(这些列按从最没有差别到最有差别的顺序排列,即OWNER列差别最小,OBJECT_TYPE次之,OBJECT_NAME列差别最大),另外还在OBJECT_NAMEOBJECT_TYPEOWNER上创建了另一个索引:

ops$tkyte@ORA10GR1> create table t

2 as

3 select * from all_objects;

Table created.

 

ops$tkyte@ORA10GR1> create index t_idx_1 on t(owner,object_type,object_name);

Index created.

 

ops$tkyte@ORA10GR1> create index t_idx_2 on t(object_name,object_type,owner);

Index created.

 

ops$tkyte@ORA10GR1> select count(distinct owner), count(distinct object_type),

2     count(distinct object_name ), count(*)

3 from t;

 

DISTINCTOWNER DISTINCTOBJECT_TYPE DISTINCTOBJECT_NAME    COUNT(*)

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

                            28                                           36                                       28537           48243

现在,为了显示这二者在高效使用空间方面难分伯仲,下面测量它们的空间利用情况:

ops$tkyte@ORA10GR1> analyze index t_idx_1 validate structure;

Index analyzed.

 

ops$tkyte@ORA10GR1> select btree_space, pct_used, opt_cmpr_count, opt_cmpr_pctsave

2 from index_stats;

 

BTREE_SPACE            PCT        OPT_CMPR_COUNT       OPT_CMPR_PCTSAVE

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

            2702744             89.0                                            2                                              28

ops$tkyte@ORA10GR1> analyze index t_idx_2 validate structure;

Index analyzed.

 

ops$tkyte@ORA10GR1> select btree_space, pct_used, opt_cmpr_count, opt_cmpr_pctsave

2 from index_stats;

 

BTREE_SPACE            PCT        OPT_CMPR_COUNT       OPT_CMPR_PCTSAVE

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

            2702744             89.0                                            1                                              11.

它们使用的空间大小完全一样,细到字节级都一样,二者没有什么区别。不过,如果使用索引键压缩,第一个索引更可压缩,这一点由OPT_CMP_PCTSAVE值可知。有人提倡索引中应该按最没有差别到最有差别的顺序来安排列,这正是这种看法的一个理由。下面来看这两个索引的表现,从而确定是否有哪个索引更“优秀”,总比另一个索引更高效。要测试这一点,我们将使用一个PL/SQL代码块(其中包括有提示的查询,指示要使用某个索引或者另一个索引):

ops$tkyte@ORA10GR1> alter session set sql_trace=true;

Session altered.

ops$tkyte@ORA10GR1> declare

2     cnt int;

3 begin

4     for x in ( select /*+FULL(t)*/ owner, object_type, object_name from t )

5     loop

6         select /*+ INDEX( t t_idx_1 ) */ count(*) into cnt

7         from t

8         where object_name = x.object_name

9             and object_type = x.object_type

11.         and owner = x.owner;

11.

11.     select /*+ INDEX( t t_idx_2 ) */ count(*) into cnt

11.     from t

11.     where object_name = x.object_name

11.         and object_type = x.object_type

11.         and owner = x.owner;

11. end loop;

11. end;

11. /

PL/SQL procedure successfully completed.

这些查询按索引读取表中的每一行。TKPROF报告显示了以下结果:

SELECT /*+ INDEX( t t_idx_1 ) */ COUNT(*) FROM T

WHERE OBJECT_NAME = :B3 AND OBJECT_TYPE = :B2 AND OWNER = :B1

 

call                count        cpu    elapsed        disk         query      current          rows

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

Parse                    1       0.00           0.00             0                 0                 0                 0

Execute      48243     11..63         11..78             0                 0                 0                 0

Fetch           48243       11.90           11.77             0     145133                 0       48243

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

total             96487     11..53         11..55             0     145133                 0       48243

          Rows   Row Source Operation

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

         48243   SORT AGGREGATE (cr=145133 pr=0 pw=0 time=2334197 us)

         57879   INDEX RANGE SCAN T_IDX_1 (cr=145133 pr=0 pw=0 time=1440672 us)(object...

********************************************************************************

SELECT /*+ INDEX( t t_idx_2 ) */ COUNT(*) FROM T

WHERE OBJECT_NAME = :B3 AND OBJECT_TYPE = :B2 AND OWNER = :B1

 

call                count        cpu    elapsed        disk         query      current          rows

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

Parse                    1       0.00           0.00             0                 0                 0                 0

Execute      48243     11..00         11..78             0                 0                 0                 0

Fetch           48243       11.87        2.11. 0 145168                 0       48243

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

total             96487     11..87         11..88             0     145168                 0       48243

          Rows   Row Source Operation

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

         48243   SORT AGGREGATE (cr=145168 pr=0 pw=0 time=2251857 us)

         57879   INDEX RANGE SCAN T_IDX_2 (cr=145168 pr=0 pw=0 time=1382547 us)(object...

它们处理的行数完全相同,而且块数也非常类似(之所以存在微小的差别,这是因为表中的行序有些偶然性,而且Oracle相应地会做一些优化),它们使用了同样的CPU时间,而且在大约相同的耗用时间内运行(再运行这个测试,CPUELAPSED这两个数字会有一点差别,但是平均来讲它们是一样的)。按照各个列的差别大小来安排这些列在索引中的顺序并不会获得本质上的效率提升,另外如前所示,如果再考虑到索引键压缩,可能还更倾向于把最没有选择性的列放在最前面。如果对索引采用COMPRESS 2,再运行前面的例子,你会发现,对于给定情况下的这个查询,第一个查询执行的I/O次数大约是后者的2/3

不过事实上,对于是把C1列放在C2列之前,这必须根据如果使用索引来决定。如果有大量如下的查询:

select * from t where c1 = :x and c2 = :y;

select * from t where c2 = :y;

那么在T(C2,C1)上建立索引就更合理。以上这两个查询都可以使用这个索引。另外,通过使用索引键压缩(我们在介绍IOT时讨论过,后面还将进一步分析),如果C2在前,就能建立一个更小的索引。这是因为,C2的各个值会在索引中平均重复4次。如果C1C2的平均长度都是11.字节,那么按道理这个索引的条目就是2,000,000字节(100,000×20)。倘若在(C2,C1)上使用索引键压缩,可以把这个索引收缩为11.250,000100,000×11..5)字节,因为C24次重复中有3次都可以避免。

Oracle 5中(不错,确实是“古老的”Oracle 5!),曾经认为应该把最有选择性的列放在索引的最前面。其理由缘于Oracle 5实现索引压缩的方式(不同于索引键压缩)。这个特性在Oracle 6中就已经去掉了,因为Oracle 6中增加了行级锁。从那以后,“把最有差别的列放在索引最前面会使索引更小或更有效率”的说法不再成立。看上去好像是这样,但实际上并非如此。如果利用索引键压缩,则恰恰相反,因为反过来才会使索引更小(即把最没有差别的列放在索引最前面)。不过如前所述,还是应该根据如何使用索引来做出决定。

11.7   小结

这一章中,我们介绍了Oracle必须提供的不同类型的索引。首先讨论了基本的B*树索引,并介绍了这种索引的几种子类型,如反向键索引(为Oracle RAC所设计)和降序索引(来获取按升序和降序混合排序的数据)。我们还花了一些时间来讨论什么时候应当使用索引,另外解释了为什么某些情况下索引可能没有用。

然后我们介绍了位图索引,在数据仓库环境(即读密集型环境,而不是OLTP)中,这对于为低到中基数的数据建立索引是一个绝好的方法。我们介绍了在哪些情况想适于使用位图索引,并解释了为什么在OLTP环境(或多个用户必须并发地更新同一个列的任何环境)中不应该考虑使用位图索引。

接下来转向基于函数的索引,这实际上是B*树索引和位图索引的特例。基于函数的索引允许我们在一个列(或多个列)的函数上创建索引,这说明可以预先计算和存储复杂计算和用户编写的函数的结果,以便以后以极快的速度完成索引获取。我们介绍了有关基于函数的索引的一些重要的实现细节,如必须有一些必要的系统级和会话级设置才能使用基于函数的索引。接下来分别在内置Oracle函数和用户编写的函数上举了两个基于函数的索引例子。最后,我们谈到了关于基于函数的索引的一些警告。

然后分析了一个非常特定的索引类型,这称为应用域索引。在此没有深入地介绍如何从头构建这种形式的索引(这个过程很长,也很复杂),而是介绍了Oracle所实现的一个例子:文本索引。

最后我回答了一些关于索引最常问的问题,还澄清了有关索引的一些神话。这一节不仅涵盖了一些简单的问题,如“能在视图中使用索引吗?”,也涉及一些更复杂的神话,如“索引中从不重用空间”。我们主要是通过具体的例子来回答这些问题,揭穿上述神话,并在此过程中展示有关的概念。

 
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 手机丢了里边有穿内衣照片怎么办 脸上不知是过敏还是湿疹流水怎么办 一个月的宝宝鼻子不通气怎么办 六个月的宝宝有清水鼻涕怎么办 一岁三个月宝宝流清鼻涕怎么办 宝宝流清水鼻涕怎么办最简单方法 8个月宝宝流清鼻涕怎么办 7个月宝宝流清鼻涕怎么办 9个月宝宝流清鼻涕怎么办 十一个月宝宝流清鼻涕怎么办 18个月宝宝咳嗽有痰怎么办 2个月宝宝鼻子不通气怎么办 两个月的宝宝鼻塞不通气怎么办 两个月大的宝宝鼻子不通气怎么办 宝宝6个月咳嗽有痰怎么办 6个月的宝宝有痰怎么办 小孩流有点咳嗽和脓鼻涕怎么办 6个月的宝宝流鼻涕该怎么办? 小孩鼻塞不流鼻涕总吸鼻子怎么办 四个多月宝宝感冒鼻塞严重怎么办 怀孕6个月严重感冒鼻塞怎么办 16个月的宝宝上火流鼻血怎么办 小狗咳嗽打了针还不好怎么办 孕妇感冒后鼻涕带血口腔发炎怎么办 宝宝出生17天睡眠不安稳怎么办 月子里宝宝睡觉老是睡不安稳怎么办 孕妇晚期咳嗽鼻涕黄咽喉痛怎么办 狗狗流脓鼻涕拉稀没食欲怎么办 孕妇眼睛充血很快就有眼屎怎么办 婴儿的眼睛流泪生眼屎怎么办呀 刚出生的婴儿眼睛有眼屎怎么办 25天的婴儿鼻子有鼻屎不通怎么办 一个月的宝宝好多鼻屎怎么办 隆鼻取线的时候好多鼻屎怎么办 小孩流浓鼻涕怎么办最简单方法 小孩流黄鼻涕怎么办最简单方法 一岁八个月宝宝鼻涕和痰多怎么办 空调铜管过不了预埋管的弯头怎么办 如果朝鲜和韩国打起来中国怎么办 寄信时不知道对方的邮编怎么办 地下钱庄转账后银行户被冻结怎么办