3. 索引组织表
3.1. 索引概述
索引是与表或表簇关联的可选结构,有时可以加快数据访问速度。通过在表的一个或多个列上创建索引,在某些情况下,您可以从表中检索一小部分随机分布的行。索引是减少磁盘I/O的众多方法之一。
如果堆组织表没有索引,那么数据库必须执行全表扫描才能找到一个值。例如,没有索引的情况下,查询 hr.departments
表中 location=2700
的记录需要数据库搜索每个表块中的每一行。这种方法随着数据量的增加,扩展性很差。
打个比方,假设一位人力资源经理有一排纸箱。员工信息的文件夹随机插入这些箱子中。Whalen(ID 200)的文件夹在箱子1从底部数起第10个位置,而King(ID 100)的文件夹在箱子3的底部。为了找到一个文件夹,经理需要从箱子1的底部逐一查看每一个文件夹,然后逐个箱子查找,直到找到目标文件夹。为了加快查找速度,经理可以创建一个索引,按顺序列出每个员工ID及其文件夹位置:
ID 100: Box 3, position 1 (bottom)
ID 101: Box 7, position 8
ID 200: Box 1, position 10 . . .
类似地,经理可以为员工的姓氏、部门ID等分别创建独立的索引。这样,无论是按员工ID、姓氏还是部门ID查找,经理都可以通过相应的索引快速定位到所需的文件夹位置,而无需逐一查找每个文件夹。
一般来说,在以下任何情况下,都可以考虑在列上创建索引:
- 索引列经常被查询 并且返回表中总行数的一小部分。
- 索引列存在参照完整性约束。索引可以避免在更新父表主键、合并到父表或从父表删除时,所需的全表锁。
- 在表上放置唯一键约束,并且您希望手动指定索引及所有索引选项。
3.1.1. 索引特征
索引是独立于其关联对象中的数据的逻辑和物理独立的模式对象。因此,可以在不实际影响索引表的情况下删除或创建索引。
注意:如果删除索引,应用程序仍然可以正常工作。然而,访问之前已索引的数据可能会变慢。
索引的存在与否不需要更改任何SQL语句的措辞。索引是一条快速访问单行数据的路径。它只影响执行速度。给定已索引的数据值,索引会直接指向包含该值的行的位置。
数据库在索引创建后会自动维护和使用这些索引。数据库还会自动将数据的更改(例如添加、更新和删除行)反映在所有相关索引中,无需用户执行额外操作。即使插入行,索引数据的检索性能仍保持几乎恒定。然而,表上存在过多索引会降低DML(数据操作语言)性能,因为数据库还必须更新这些索引。
索引具有以下属性:
-
可用性:索引可以是可用的(默认)或不可用的。不可用的索引不会被DML操作维护,并且会被优化器忽略。不可用的索引可以提高批量加载的性能。与其删除索引后再重新创建,不如将索引设为不可用,然后重建它。不可用的索引和索引分区不占用空间。当您将一个可用的索引设为不可用时,数据库会删除其索引段。
-
可见性:索引可以是可见的(默认)或不可见的。不可见的索引会被DML操作维护,但默认情况下不会被优化器使用。将索引设为不可见是替代将其设为不可用或删除它的一种方法。不可见的索引在删除前进行测试或临时使用索引而不影响整个应用程序时特别有用。
3.1.1.1. 索引键和列
键是可以用于构建索引的一组列或表达式。尽管术语经常互换使用,但索引和键是不同的。索引是存储在数据库中的结构,用户可以使用SQL语句管理它们。键严格来说是一个逻辑概念。
以下语句在示例表 oe.orders 的 customer_id 列上创建一个索引:
CREATE INDEX ord_customer_ix ON orders (customer_id);
在上述语句中,customer_id
列是索引键。索引本身被命名为 ord_customer_ix
。
注意:主键和唯一键会自动创建索引,但您可能需要在外键上创建索引。
3.1.1.2. 复合索引
复合索引,也称为连接索引,是在表的多个列上创建的索引。复合索引中的列应按照最适合查询检索数据的顺序排列,而不必在表中相邻。
复合索引可以加速 SELECT
语句中 WHERE
子句引用了所有或前导部分复合索引列的数据检索。因此,定义中列的顺序非常重要。一般来说,最常访问的列应放在前面。
例如,假设一个应用程序经常查询employees
表中的last_name
、job_id
和salary
列。并且假设last_name
具有高基数,这意味着相对于表的行数,不同值的数量很大。您可以按以下列顺序创建索引:
CREATE INDEX employees_ix ON employees (last_name, job_id, salary);
访问所有三列、仅 last_name
列或仅 last_name
和 job_id
列的查询会使用此索引。在此示例中,不访问 last_name
列的查询不会使用该索引。
注意:在某些情况下,例如当前导列的基数非常低时,数据库可能会使用跳跃扫描(skip scan)来使用该索引。
如果每个索引的列排列方式不同,则同一张表可以有多个索引。如果指定不同的列排列顺序,可以使用相同的列创建多个索引。例如,以下SQL语句指定了有效的排列方式:
CREATE INDEX employee_idx1 ON employees (last_name, job_id);
CREATE INDEX employee_idx2 ON employees (job_id, last_name);
3.1.1.3. 唯一和非唯一索引
索引可以是唯一索引或非唯一索引。唯一索引保证表中的任意两行在键列或键列组合中没有重复的值。例如,没有两个员工可以有相同的员工ID。因此,在唯一索引中,每个数据值只有一个行号(rowid)。叶块中的数据仅按键排序。
非唯一索引允许索引列或列组合中存在重复值。例如,employees
表中的 first_name
列可能包含多个 "Mike" 值。对于非唯一索引,行号(rowid)被包含在键中并按排序顺序排列,因此非唯一索引按索引键和行号(升序)排序。
Oracle数据库不会对所有键列均为NULL的表行进行索引,除非是位图索引或当簇键列值为NULL时。
3.1.1.4. 索引类型
Oracle 数据库提供了几种索引方案,提供了互补的性能功能。索引可以分类如下:
-
B-树索引
这些索引是标准的索引类型。它们非常适合主键和高度选择性的索引。作为串联索引使用时,B-树索引可以按索引列排序检索数据。B-树索引有以下子类型:- 索引组织表(Index-organized tables)
索引组织表与堆组织表不同,因为数据本身就是索引。 - 反向键索引(Reverse key indexes)
在这种类型的索引中,索引键的字节被反转,例如,103 存储为 301。字节的反转将插入分散到多个块中。 - 降序索引(Descending indexes)
这种类型的索引按降序存储特定列或列的值。 - B-树簇索引(B-tree cluster indexes)
这种类型的索引用于索引表簇键。键指向包含与簇键相关的行的块,而不是行。
- 索引组织表(Index-organized tables)
-
位图和位图连接索引
在位图索引中,索引条目使用位图指向多个行。相反,B树索引条目指向单个行。位图连接索引是用于两个或多个表连接的位图索引。 -
函数索引
这种类型的索引包括通过函数(如UPPER函数)转换的列,或包含在表达式中的列。B树索引或位图索引都可以是函数索引。 -
应用域索引
这种类型的索引由用户为特定应用领域的数据创建。物理索引不需要使用传统的索引结构,可以存储在Oracle数据库中作为表,也可以作为文件存储在外部。
3.1.2. B-Tree索引
B树索引(平衡树的简称)是最常见的数据库索引类型。B树索引是一个按值排序的列表,这些值被分为不同的范围。通过将一个键与一行或一行范围相关联,B树在广泛的查询中(包括精确匹配和范围搜索)提供了优异的检索性能。
图3-1展示了B树索引的结构。该示例显示了在department_id列上的索引,该列是employees表中的一个外键列。
3.1.2.1. 分支块和叶子块
B树索引有两种类型的块:用于搜索的分支块和存储值的叶子块。B树索引的上层分支块包含指向下层索引块的索引数据。在图3-1中,根分支块有一个条目0-40,指向下一个分支级别中的最左侧块。这个分支块包含诸如0-10和11-19的条目。每个条目指向一个包含在该范围内的键值的叶子块。
B树索引是平衡的,因为所有叶子块自动保持在相同的深度。因此,从索引的任何位置检索任何记录所需的时间大致相同。索引的高度是从根块到叶子块所需的块数。分支级别是高度减去1。在图3-1中,索引的高度为3,分支级别为2。
分支块存储在两个键之间做出分支决策所需的最小键前缀。这种技术使数据库能够在每个分支块上容纳尽可能多的数据。分支块包含指向包含该键的子块的指针。键和指针的数量受到块大小的限制。
叶子块包含每个索引数据值和用于定位实际行的相应rowid。每个条目按(键,rowid)排序。在叶子块内,键和rowid与其左右的兄弟条目相链接。叶子块本身也双向链接。在图3-1中,最左侧的叶子块(0-10)链接到第二个叶子块(11-19)。
注意:带有字符数据的列中的索引基于数据库字符集中的字符的二进制值。
3.1.2.2. 索引扫描
在索引扫描中,数据库通过遍历索引并使用语句中指定的索引列值来检索一行。如果数据库扫描索引以查找一个值,那么它将在n次I/O操作中找到这个值,其中n是B树索引的高度。这是Oracle数据库索引背后的基本原理。
如果SQL语句仅访问索引列,那么数据库直接从索引中读取值,而不是从表中读取值。如果语句访问索引列之外的列,那么数据库使用rowid在表中找到这些行。通常,数据库通过交替读取索引块和表块来检索表数据。
1. 全索引扫描(Full Index Scan)
在全索引扫描中,数据库按顺序读取整个索引。如果SQL语句中的谓词(WHERE子句)引用了索引中的列,并且在某些情况下未指定谓词,全索引扫描是可行的。全索引扫描可以消除排序,因为数据按索引键排序。
假设一个应用程序运行以下查询:
SELECT department_id, last_name, salary FROM employees WHERE salary > 5000 ORDER BY department_id, last_name;
还假设department_id、last_name和salary是索引中的复合键。Oracle数据库执行索引的全扫描,按排序顺序读取(按部门ID和姓氏排序),并在salary属性上进行筛选。这样,数据库扫描的数据集比employees表更小,因为employees表包含的列比查询中包含的列更多,并且避免了对数据进行排序。
例如,全扫描可以按以下方式读取索引条目:
50,Atkinson,2800,rowid
60,Austin,4800,rowid
70,Baer,10000,rowid
80,Abel,11000,rowid
80,Ande,6400,rowid
110,Austin,7200,rowid
. . .
2、快速全索引扫描(Fast Full Index Scan)
快速全索引扫描是指数据库在不访问表的情况下直接访问索引中的数据,并且数据库读取索引块时没有特定的顺序。
快速全索引扫描在满足以下两个条件时是全表扫描的替代方法:
■ 索引必须包含查询所需的所有列。
■ 查询结果集中不得出现包含全部空值的行。为了保证这一结果,索引中的至少一列必须满足以下条件之一:
– 具有 NOT NULL 约束
– 应用了防止空值在查询结果集中被考虑的谓词
例如,一个应用程序发出了以下查询,该查询不包含 ORDER BY 子句:
SELECT last_name, salary FROM employees;
last_name
列具有 not null 约束。如果 last_name
和 salary
作为索引中的复合键,那么快速全索引扫描可以读取索引条目以获取所需信息:
Baida,2900,rowid
Zlotkey,10500,rowid
Austin,7200,rowid
Baer,10000,rowid
Atkinson,2800,rowid
Austin,4800,rowid
. . .
3、索引范围扫描(Index Range Scan)
索引范围扫描是一种有序的索引扫描,具有以下特征:
■ 在条件中指定了一个或多个索引的前导列。条件是一个或多个表达式和逻辑(布尔)运算符的组合,返回TRUE、FALSE或UNKNOWN的值。
对于一个索引键,可能存在0、1或多个值。
■ 数据库通常使用索引范围扫描来访问选择性的数据。选择性是指查询选择的表中行的百分比,0表示没有行,1表示所有行。选择性与查询谓词有关,例如WHERE last_name LIKE 'A%',或多个谓词的组合。当值接近0时,谓词变得更具选择性;当值接近1时,谓词变得不太选择性(或者说更不具选择性)。
例如,用户查询姓氏以"A"开头的员工。假设last_name列已建立索引,且索引条目如下:
Abel, rowid
Ande, rowid
Atkinson, rowid
Austin, rowid
Austin, rowid
Baer, rowid
...
数据库可以使用范围扫描,因为 last_name
列在谓词中指定,每个索引键可能有多个rowid。例如,有两名员工名叫Austin,因此与键Austin关联有两个rowid。
索引范围扫描可以在两侧都有界限,如在查询ID在10到40之间的部门时,或者只在一侧有界限,如在查询ID超过40的查询中。为了扫描索引,数据库通过叶块向后或向前移动。例如,对于ID在10到40之间的扫描,首先找到包含最低键值大于或等于10的第一个索引叶块。然后,扫描横向通过叶节点的链表,直到找到一个大于40的值。
4、索引唯一扫描(Index Unique Scan)
与索引范围扫描相比,索引唯一扫描(Index Unique Scan)必须只有0或1个rowid与索引键关联。当谓词使用等号操作符引用唯一索引键中的所有列时,数据库会执行唯一扫描。索引唯一扫描在找到第一条记录后便停止处理,因为不可能存在第二条记录。
例如,假设用户运行以下查询:
SELECT * FROM employees WHERE employee_id = 5;
假设employee_id
列是主键,并且具有以下索引条目:
1, rowid
2, rowid
4, rowid
5, rowid
6, rowid
...
在这种情况下,数据库可以使用索引唯一扫描来定位ID
为5的员工的rowid。
5、索引跳跃扫描(Index Skip Scan)
索引跳跃扫描(Index Skip Scan)使用复合索引的逻辑子索引。数据库在单个索引中“跳跃”,就像在搜索多个独立的索引一样。当复合索引的前导列只有少数不同值,而非前导键有许多不同值时,跳跃扫描非常有利。
当复合索引的前导列未在查询谓词中指定时,数据库可能选择使用索引跳跃扫描。例如,假设您在sh.customers
表中运行以下查询以查找某位客户:
SELECT * FROM sh.customers WHERE cust_email = 'Abbey@company.com';
在这种情况下,如果复合索引的前导列未指定,但非前导列cust_email
在查询中被使用,数据库可以选择使用索引跳跃扫描来查找相关数据。
customers
表有一列cust_gender
,其值为M
或F
。假设在列(cust_gender, cust_email)
上存在一个复合索引。示例3-1显示了部分索引条目。
示例 3-1 复合索引条目
F, Wolf@company.com, rowid
F, Wolsey@company.com, rowid
F, Wood@company.com, rowid
F, Woodman@company.com, rowid
F, Yang@company.com, rowid
F, Zimmerman@company.com, rowid
M, Abbassi@company.com, rowid
M, Abbey@company.com, rowid
...
即使在WHERE
子句中未指定cust_gender
,数据库仍可以使用该索引的跳跃扫描(Skip Scan)。
在跳跃扫描中,逻辑子索引的数量由前导列中不同值的数量决定。在示例3-1中,前导列有两个可能的值。数据库逻辑上将索引分为两个子索引,一个具有键F
,另一个具有键M
。
当搜索电子邮件为Abbey@company.com
的客户记录时,数据库首先搜索值为F
的子索引,然后搜索值为M
的子索引。从概念上讲,数据库按照以下方式处理查询:
SELECT * FROM sh.customers WHERE cust_gender = 'F' AND cust_email = 'Abbey@company.com'
UNION ALL
SELECT * FROM sh.customers WHERE cust_gender = 'M' AND cust_email = 'Abbey@company.com';
6、索引聚簇因子(Index Clustering Factor)
索引聚簇因子衡量行顺序与某个索引值(如员工的姓氏)之间的关系。在行存储中针对该值的顺序越多,聚簇因子就越低。
聚簇因子大致用于衡量通过索引读取整个表所需的I/O操作次数:
-
如果聚簇因子较高,则Oracle数据库在进行大范围的索引扫描时会执行相对较多的I/O操作。索引条目指向随机的表块,因此数据库可能需要一遍又一遍地读取相同的块,以检索索引指向的数据。
-
如果聚簇因子较低,则Oracle数据库在进行大范围的索引扫描时会执行相对较少的I/O操作。范围内的索引键通常指向相同的数据块,因此数据库不需要反复读取相同的块。
聚簇因子与索引扫描相关,因为它可以表明:
- 数据库是否会在大范围扫描中使用索引
- 表相对于索引键的组织程度
- 如果需要按照索引键顺序排列行,是否应该考虑使用索引组织表(Index-Organized Table)、分区(Partitioning)或表簇(Table Cluster)
例如,假设employees
表可以存放在两个数据块中。表3-1显示了这两个数据块中的行(省略号表示未显示的数据)。
数据块中的行按姓氏排序(加粗显示)
例如,数据块1中的底部行描述了Abel,接下来的行描述了Ande,以此类推,直到数据块1的顶部行描述了Steven King。数据块2的底部行描述了Kochar,接下来的行描述了Kumar,以此类推,直到数据块2的最后一行描述了Zlotkey。
假设在last_name
列上存在一个索引。概念上,索引条目可能如下所示:
Abel, block1row1
Ande, block1row2
Atkinson, block1row3
Austin, block1row4
Baer, block1row5
...
假设还存在一个独立的员工ID列索引。概念上,这个索引条目可能如下所示,员工ID在两个数据块中几乎是随机分布的:
100, block1row50
101, block2row1
102, block1row9
103, block2row19
104, block2row39
105, block1row4
...
示例 3-2 查询ALL_INDEXES
视图以获取这两个索引的聚簇因子。EMP_NAME_IX
的聚簇因子较低,这意味着单个叶块中的相邻索引条目通常指向同一数据块中的行。EMP_EMP_ID_PK
的聚簇因子较高,这意味着相同叶块中的相邻索引条目更不可能指向同一数据块中的行。
Example 3–2 Clustering Factor
SQL> SELECT INDEX_NAME, CLUSTERING_FACTOR
2 FROM ALL_INDEXES
3 WHERE INDEX_NAME IN ('EMP_NAME_IX','EMP_EMP_ID_PK');
INDEX_NAME CLUSTERING_FACTOR
-------------------- ----------------
EMP_EMP_ID_PK 19 EMP_NAME_IX 2
3.1.2.3. 索引扫描反向键索引(Reverse Key Indexes)
反向键索引是一种B树索引类型,它在物理上反转每个索引键的字节,同时保持列的顺序。例如,如果索引键是20,并且在标准B树索引中以十六进制形式存储的两个字节是C1,15,则反向键索引将字节存储为15,C1。
反转键可以解决B树索引右侧叶块的争用问题。在Oracle Real Application Clusters(Oracle RAC)数据库中,这个问题尤为严重,因为多个实例会重复修改相同的块。例如,在一个订单表中,订单的主键是顺序生成的。在集群中的一个实例添加订单20,而另一个实例添加订单21,这两个实例将主键写入索引右侧的同一叶块中。
在反向键索引中,字节顺序的反转使得插入操作分布在所有叶键中。例如,像20和21这样的键在标准键索引中本会相邻存储,而在反向键索引中则被存储在相隔很远的不同块中。因此,顺序键的插入I/O操作得到了更均匀的分布。
由于索引中的数据在存储时不是按列键排序的,反向键排列在某些情况下会限制索引范围扫描查询的能力。例如,如果用户发出查询,查找大于20的订单ID,则数据库不能从包含该ID的块开始,并在叶块中水平扫描。
升序和降序索引(Ascending and Descending Indexes)
在升序索引中,Oracle数据库以升序存储数据。默认情况下,字符数据按每个字节的二进制值排序,数字数据从最小到最大排序,日期数据从最早到最新排序。
3.1.2.4. 升序和降序索引(Ascending and Descending Indexes)
在升序索引中,Oracle数据库以升序存储数据。默认情况下,字符数据按每个字节的二进制值排序,数字数据从最小到最大排序,日期数据从最早到最新排序。
升序索引示例:
CREATE INDEX emp_deptid_ix ON hr.employees(department_id);
在这个例子中,Oracle数据库根据department_id
列对hr.employees
表进行排序。数据库将升序索引加载有department_id
和相应的rowid
值,按升序排列,从0开始。当使用索引时,Oracle数据库会搜索已排序的department_id
值,并使用关联的rowid
来定位具有请求的department_id
值的行。
通过在CREATE INDEX
语句中指定DESC
关键字,可以创建降序索引。在这种情况下,索引在指定列或列组上以降序存储数据。例如,如果employees.department_id
列上的索引为降序,则包含值250的叶块将位于树的左侧,而包含值0的叶块将位于右侧。默认情况下,通过降序索引的搜索是从最高值到最低值。
降序索引在查询对某些列升序排序而对其他列降序排序时非常有用。例如,假设您创建了一个复合索引,包含last_name
和department_id
列,如下所示:
CREATE INDEX emp_name_dpt_ix ON hr.employees(last_name ASC, department_id DESC);
如果用户查询hr.employees
表,要求last_name
按升序(A到Z)排序,而department_id
按降序(高到低)排序,则数据库可以利用这个索引来检索数据,从而避免了额外的排序步骤。使用这种索引,数据库可以直接按所需的顺序提取数据,提升查询性能。
3.1.2.5. 键压缩(Key Compression)
Oracle数据库可以使用键压缩来压缩B树索引或索引组织表中的主键列值的部分内容。键压缩可以显著减少索引所消耗的空间。
一般而言,索引键包含两部分:分组部分和唯一部分。键压缩将索引键拆分为前缀条目(分组部分)和后缀条目(唯一或几乎唯一的部分)。数据库通过在索引块中共享前缀条目来实现压缩。
注意: 如果键未定义为具有唯一部分,则数据库通过将rowid
附加到分组部分来提供唯一性。
默认情况下,唯一索引的前缀由除最后一列之外的所有键列组成,而非唯一索引的前缀由所有键列组成。例如,假设您在oe.orders
表上创建了一个复合索引,如下所示:
CREATE INDEX orders_mod_stat_ix ON orders (order_mode, order_status);
在order_mode
和order_status
列中有许多重复的值。一个索引块的条目可能如示例3-3所示:
示例 3-3: 订单表中的索引条目
online,0,AAAPvCAAFAAAAFaAAa
online,0,AAAPvCAAFAAAAFaAAg
online,0,AAAPvCAAFAAAAFaAAl
online,2,AAAPvCAAFAAAAFaAAm
online,3,AAAPvCAAFAAAAFaAAq
online,3,AAAPvCAAFAAAAFaAAt
在示例3-3中,键前缀将由order_mode
和order_status
值的连接组成。如果这个索引是使用默认的键压缩创建的,那么像online,0
和online,2
这样的重复键前缀将被压缩。概念上,数据库的压缩效果如下:
online,0
AAAPvCAAFAAAAFaAAa
AAAPvCAAFAAAAFaAAg
AAAPvCAAFAAAAFaAAl
online,2
AAAPvCAAFAAAAFaAAm
online,3
AAAPvCAAFAAAAFaAAq
AAAPvCAAFAAAAFaAAt
后缀条目构成了索引行的压缩版本。每个后缀条目引用一个前缀条目,前缀条目与后缀条目存储在同一个索引块中。创建压缩索引时,您还可以指定前缀长度。例如,如果您指定前缀长度为1,则前缀将是order_mode
,后缀将是order_status,rowid
。对于示例3-3中的值,索引将重复出现的online
提取如下:
online
0,AAAPvCAAFAAAAFaAAa
0,AAAPvCAAFAAAAFaAAg
0,AAAPvCAAFAAAAFaAAl
2,AAAPvCAAFAAAAFaAAm
3,AAAPvCAAFAAAAFaAAq
3,AAAPvCAAFAAAAFaAAt
索引在每个叶块中最多存储一个特定的前缀。只有在B树索引的叶块中的键才会被压缩。在分支块中,键后缀可能会被截断,但键本身并未被压缩。
3.1.3. 位图索引(Bitmap Indexes)
位图索引(Bitmap Index)
在位图索引中,数据库为每个索引键存储一个位图。在传统的B树索引中,一个索引条目指向一行。而在位图索引中,每个索引键存储指向多行的指针。
位图索引主要是为数据仓库或查询以随意方式引用多个列的环境而设计的。以下情况可能适合使用位图索引:
- 被索引的列具有低基数,即与表中的行数相比,不同值的数量较少。
- 被索引的表是只读的,或者不受DML(数据操作语言)语句的显著修改。
例如,在数据仓库中,sh.customers
表有一个cust_gender
列,只有两个可能的值:M和F。假设经常查询某个性别的客户数量,那么customers.cust_gender
列就适合创建位图索引。
在位图中,每个位对应一个可能的rowid
。如果该位被设置,则表示具有相应rowid
的行包含该键值。一个映射函数将位位置转换为实际的rowid
,因此尽管位图索引使用不同的内部表示形式,但它提供了与B树索引相同的功能。
如果单行中的被索引列被更新,数据库会锁定索引键条目(例如,M
或F
),而不是映射到已更新行的单个位。由于一个键指向多行,针对索引数据的DML操作通常会锁定所有这些行。因此,位图索引不适合许多在线事务处理(OLTP)应用程序。
3.1.3.1. 单表上的位图索引 (Bitmap Indexes on a Single Table)
示例 3-4 显示了对sh.customers
表的查询。此表中的某些列是位图索引的候选列。
Example 3–4 Query of customers Table
SQL> SELECT cust_id, cust_last_name, cust_marital_status, cust_gender 2 FROM sh.customers 3 WHERE ROWNUM < 8 ORDER BY cust_id;
CUST_ID CUST_LAST_ CUST_MAR C
---------- ---------- ------- -
1 Kessel M
2 Koch F
3 Emmerson M
4 Hardy M
5 Gowen M
6 Charles single F
7 Ingram single F
7 rows selected.
cust_marital_status
和cust_gender
列具有低基数,而 cust_id
和 cust_last_name
则不是。因此,位图索引可能适用于 cust_marital_status
和 cust_gender
。位图索引可能对其他列没有用处。相反,这些列上的唯一 B-tree 索引可能会提供最有效的表示和检索。
表 3-2 说明了示例 3-4 中显示的 cust_gender 列的位图索引。它由两个独立的位图组成,每个性别一个。
映射函数将位图中的每个位转换为客户表的 rowid。每个位的值取决于表中相应行的值。例如,M 值的位图在其第一个位包含 1,因为在客户表的第一行性别是 M。位图 cust_gender='M' 在第 2、6 和 7 行的位上为 0,因为这些行不包含 M 作为它们的值。
注意:与 B-tree 索引不同,位图索引可以包含完全由空值(null values)组成的键。对空值进行索引对于某些 SQL 语句可能是有用的,例如带有聚合函数 COUNT 的查询。
一位研究客户人口统计趋势的分析师可能会问:“我们有多少女性客户是单身或离婚的?”这个问题对应于以下 SQL 查询:
SELECT COUNT(*) FROM customers WHERE cust_gender = 'F' AND cust_marital_status IN ('single', 'divorced');
位图索引可以通过计算结果位图中的 1 的数量来高效处理这个查询,如表 3-3 所示。为了识别满足条件的客户,Oracle 数据库可以使用结果位图来访问表。
位图索引能够有效地合并对应于 WHERE 子句中多个条件的索引。在实际访问表之前,满足部分但不是全部条件的行将被过滤掉。这种技术提高了响应时间,通常是非常显著的。
3.1.3.2. 位图联合索引 (Bitmap Join Indexes)
位图连接索引是对两个或更多表的连接进行索引的位图索引。对于表列中的每个值,索引存储索引表中相应行的 rowid。相比之下,标准位图索引是在单个表上创建的。位图连接索引是一种通过提前执行限制来减少必须连接的数据量的有效手段。举一个位图连接索引可能有用的示例,假设用户经常查询具有特定工作类型的员工数量。一个典型的查询可能如下所示:
SELECT COUNT(*) FROM employees, jobs WHERE employees.job_id = jobs.job_id AND jobs.job_title = 'Accountant';
上述查询通常会使用 jobs.job_title 上的索引来检索会计的行,然后是工作 ID,以及 employees.job_id 上的索引来找到匹配的行。为了直接从索引本身而不是从表的扫描中检索数据,你可以如下创建一个位图连接索引:
CREATE BITMAP INDEX employees_bm_idx ON employees (jobs.job_title) FROM employees, jobs WHERE employees.job_id = jobs.job_id;
正如图 3-2 所示,索引键是 jobs.job_title,而索引表是 employees。
从概念上讲,employees_bm_idx 是示例 3-5 中 SQL 查询中 jobs.title 列的索引(包括示例输出)。索引中的 job_title 键指向 employees 表中的行。查询会计数量时,可以使用索引避免访问 employees 和 jobs 表,因为索引本身包含了请求的信息。
Example 3–5 Join of employees and jobs Tables
SELECT jobs.job_title AS "jobs.job_title", employees.rowid AS "employees.rowid"
FROM employees, jobs WHERE employees.job_id = jobs.job_id ORDER BY job_title;
jobs.job_title employees.rowid
----------------------------------- -----------------
Accountant AAAQNKAAFAAAABSAAL
Accountant AAAQNKAAFAAAABSAAN
Accountant AAAQNKAAFAAAABSAAM
Accountant AAAQNKAAFAAAABSAAJ
Accountant AAAQNKAAFAAAABSAAK
Accounting Manager AAAQNKAAFAAAABTAAH
Administration Assistant AAAQNKAAFAAAABTAAC
Administration Vice President AAAQNKAAFAAAABSAAC
Administration Vice President AAAQNKAAFAAAABSAAB
. .
在数据仓库中,连接条件通常是维度表的主键列和事实表中的外键列之间的等值连接(使用等号操作符)。位图连接索引在存储效率方面有时比物化连接视图(materialized join views)更加高效,后者是提前物化连接的一种替代方法。
3.1.3.3. 位图存储结构(Bitmap Storage Structure)
Oracle 数据库使用 B-tree 索引结构来存储每个索引键的位图。例如,如果 jobs.job_title 是位图索引的键列,那么索引数据存储在一个 B-tree 中。各个位图存储在叶块中。假设 jobs.job_title 列有唯一的值“Shipping Clerk”、“Stock Clerk”等。这个索引的位图索引条目包含以下组件:
- 作为索引键的工作标题
- 一系列 rowid 的低 rowid 和高 rowid
- 范围内特定 rowid 的位图
从概念上讲,这个索引的索引叶块可能包含如下条目:
Shipping Clerk,AAAPzRAAFAAAABSABQ,AAAPzRAAFAAAABSABZ,0010000100
Shipping Clerk,AAAPzRAAFAAAABSABa,AAAPzRAAFAAAABSABh,010010
Stock Clerk,AAAPzRAAFAAAABSAAa,AAAPzRAAFAAAABSAAc,1001001100
Stock Clerk,AAAPzRAAFAAAABSAAd,AAAPzRAAFAAAABSAAt,0101001001
Stock Clerk,AAAPzRAAFAAAABSAAu,AAAPzRAAFAAAABSABz,100001
...
相同的工作标题出现在多个条目中,因为 rowid 范围不同。
假设一个会话将一名员工的工作 ID 从“Shipping Clerk”更新为“Stock Clerk”。在这种情况下,会话需要对旧值(Shipping Clerk)和新值(Stock Clerk)的索引键条目进行独占访问。Oracle 数据库锁定这些两个条目指向的行——但不是指向“Accountant”或任何其他键的行——直到 UPDATE 提交。
位图索引的数据存储在一个段中。Oracle 数据库将每个位图存储在一个或多个部分中。每个部分占用单个数据块的一部分。这种存储方式使得位图索引在处理大量数据时非常高效,尤其是在数据仓库和 OLAP(在线分析处理)系统中,这些系统中的查询通常涉及对维度表和事实表的连接操作。
3.1.4. 基于函数的索引(Function-Based Indexes)
您可以在涉及被索引表中的一个或多个列的函数和表达式上创建索引。基于函数的索引计算涉及一个或多个列的函数或表达式的值,并将其存储在索引中。基于函数的索引可以是 B-tree 索引或位图索引。
用于构建索引的函数可以是算术表达式,也可以是包含 SQL 函数、用户定义的 PL/SQL 函数、包函数或 C 调用的表达式。例如,一个函数可以将两列的值相加。
3.1.4.1. 基于函数的索引的用途
基于函数的索引对于评估包含函数的 WHERE 子句中的语句非常有效。数据库只在查询中包含函数时才使用基于函数的索引。然而,在处理 INSERT 和 UPDATE 语句时,数据库仍然必须评估该函数。
例如,假设您创建了以下基于函数的索引:
CREATE INDEX emp_total_sal_idx ON employees (12 * salary * commission_pct, salary, commission_pct);
数据库可以在处理例如示例 3-6(部分示例输出包括在内)的查询时使用前面的索引。
示例 3-6 包含算术表达式的查询
SELECT employee_id, last_name, first_name, 12*salary*commission_pct AS "ANNUAL SAL"
FROM employees WHERE (12 * salary * commission_pct) < 30000 ORDER BY "ANNUAL SAL" DESC;
EMPLOYEE_ID LAST_NAME FIRST_NAME ANNUAL SAL
----------- ------------------------- -------------------- ---------
159 Smith Lindsey 28800
151 Bernstein David 28500
152 Hall Peter 27000
160 Doran Louise 27000
175 Hutton Alyssa 26400
149 Zlotkey Eleni 25200
169 Bloom Harrison 24000
在 SQL 函数 UPPER(column_name) 或 LOWER(column_name) 上定义的基于函数的索引有助于不区分大小写的搜索。例如,假设 employees 表中的 first_name 列包含混合大小写字符。您可以在 hr.employees 表上创建以下基于函数的索引:
CREATE INDEX emp_fname_uppercase_idx ON employees ( UPPER(first_name) );
emp_fname_uppercase_idx 索引可以促进以下查询:
SELECT * FROM employees WHERE UPPER(first_name) = 'AUDREY';
基于函数的索引还有助于对表中的特定行进行索引。例如,sh.customers 表中的 cust_valid 列的值要么是 I,要么是 A。要仅对 A 行进行索引,您可以编写一个函数,该函数对除 A 行之外的所有行返回 null 值。您可以如下创建索引:
CREATE INDEX cust_valid_idx ON customers ( CASE cust_valid WHEN 'A' THEN 'A' END );
3.1.4.2. 使用基于函数的索引进行优化
优化器可以使用基于函数的索引对 WHERE 子句中包含表达式的查询进行索引范围扫描。当谓词(WHERE 子句)的选择性较低时,范围扫描访问路径尤其有益。在示例 3-6 中,如果建立了基于表达式 12salarycommission_pct 的索引,优化器可以使用索引范围扫描。虚拟列对于加速从表达式派生的数据的访问很有用。例如,您可以将虚拟列 annual_sal 定义为 12salarycommission_pct,并在 annual_sal 上创建基于函数的索引。
优化器通过解析 SQL 语句中的表达式,然后比较语句和基于函数的索引的表达式树来执行表达式匹配。这种比较是不区分大小写的,并且忽略空格。
3.1.5. 应用域索引(Application Domain Indexes)
应用领域索引是针对特定应用程序定制的索引。Oracle 数据库提供可扩展索引以执行以下操作:
- 适应自定义、复杂数据类型的索引,如文档、空间数据、图像和视频剪辑(见第 19-11 页的“非结构化数据”)
- 利用专门的索引技术
您可以将特定于应用程序的索引管理例程封装为索引类型(indextype)模式对象,并在表列或对象类型的属性上定义领域索引。可扩展索引可以高效地处理特定于应用程序的操作符。
控制领域索引的结构和内容的应用软件称为卡匣(cartridges)。数据库与应用程序交互以构建、维护和搜索领域索引。索引结构本身可以作为索引组织表存储在数据库中,或者作为文件存储在外部。
3.1.6. 索引存储(Index Storage)
Oracle 数据库将索引数据存储在索引段中。数据块中可用于索引数据的空间是数据块大小减去块开销、条目开销、rowid,以及每个被索引值的一个长度字节。
索引段的表空间可以是所有者默认表空间,或者是在 CREATE INDEX 语句中特别指定的表空间。为了方便管理,您可以将索引存储在与其表分开的表空间中。例如,您可能选择不备份只包含可以重建的索引的表空间,从而减少备份所需的时间和存储空间。
3.2. 索引组织表概述
索引组织表是一种存储在 B-tree 索引结构变体中的表。在堆组织表中,行被插入到它们适合的地方。在索引组织表中,行存储在定义在表主键上的索引中。B-tree 中的每个索引条目还存储非键列值。因此,索引就是数据,数据就是索引。应用程序像操作堆组织表一样操作索引组织表,使用 SQL 语句。
对于索引组织表的类比,假设一个人力资源经理有一排标有数字的纸箱——1、2、3、4 等,但这些箱子并不按顺序放在架子上。相反,每个箱子都包含指向序列中下一个箱子位置的指针。
每个箱子里存放着按员工 ID 排序的员工档案夹。员工 King 的 ID 是 100,这是最低的 ID,所以他的文件夹在第一个箱子的底部。101 号员工的文件夹在 100 号上面,102 号在 101 号上面,依此类推,直到第一个箱子满了。序列中的下一个文件夹在第二个箱子的底部。
在这个类比中,按员工 ID 排序文件夹使得可以高效地搜索文件夹,而无需维护单独的索引。假设用户请求 107、120 和 122 号员工的记录。经理可以顺序搜索文件夹,并在找到时检索每个文件夹,而不是分两步先搜索索引再检索文件夹。
索引组织表通过主键或有效键前缀提供对表行的更快访问。在叶块中包含行的非键列避免了额外的数据块 I/O。例如,员工 100 的工资存储在索引行本身。此外,由于行按主键顺序存储,通过主键或前缀进行范围访问涉及最小的块 I/O。另一个好处是避免了单独主键索引的空间开销。当必须将相关数据片段存储在一起或必须按特定顺序物理存储数据时,索引组织表非常有用。这种类型的表通常用于信息检索、空间(见第 19-14 页的“Oracle 空间概述”)和 OLAP 应用程序(见第 17-20 页的“OLAP”)。
3.2.1. 索引组织表特征
数据库系统通过对 B-tree 索引结构的操作来执行索引组织表的所有操作。表 3-4 总结了索引组织表和堆组织表之间的差异。
图 3-3 展示了一个索引组织部门表的结构。叶块包含按主键顺序排列的表行。例如,第一个叶块中的第一个值显示了部门 ID 为 20,部门名称为 Marketing,经理 ID 为 201,位置 ID 为 1800。
索引组织表将所有数据存储在相同的结构中,并且不需要存储 rowid。如图 3-3 所示,索引组织表的一个叶块可能包含以下按主键排序的条目:
20,Marketing,201,1800
30,Purchasing,114,1700
索引组织表的第二个叶块可能包含以下条目:
50,Shipping,121,1500
60,IT,103,1400
按主键顺序扫描索引组织表的行将按以下顺序读取块:1. 块 1 2. 块 2
为了对比堆组织表和索引组织表中的数据访问,假设堆组织部门表段的块 1 包含如下行:
50,Shipping,121,1500
20,Marketing,201,1800
块 2 包含同一表的如下行:
30,Purchasing,114,1700
60,IT,103,1400
对于这个堆组织表,B-tree 索引的叶块包含以下条目,其中第一个值是主键,第二个是 rowid:
20,AAAPeXAAFAAAAAyAAD
30,AAAPeXAAFAAAAAyAAA
50,AAAPeXAAFAAAAAyAAC
60,AAAPeXAAFAAAAAyAAB
按主键顺序扫描表行将按以下顺序读取表段块:
- 块 1
- 块 2
- 块 1
- 块 2
因此,在本例中,块 I/O 的数量是索引组织示例中的两倍。
3.2.2. 索引组织表具有行溢出区域
在创建索引组织表时,您可以指定一个单独的段作为行溢出区域。在索引组织表中,B-tree 索引条目可能很大,因为它们包含整行数据,因此有一个单独的段来包含这些条目是有用的。相比之下,B-tree 条目通常很小,因为它们由键和 rowid 组成。如果指定了行溢出区域,那么数据库可以将索引组织表中的行分成以下部分:
- 索引条目
这部分包含所有主键列的列值,一个指向行溢出部分的物理 rowid,以及可选的一些非键列。这部分存储在索引段中。 - 溢出部分
这部分包含剩余非键列的列值。这部分存储在溢出存储区段中。
3.2.3. 索引组织表上的辅助索引
辅助索引是索引组织表上的一个索引。从某种意义上说,它是索引上的一个索引。辅助索引是一个独立的模式对象,并且与索引组织表分开存储。
如第2-13页“Rowid数据类型”中所解释的,Oracle数据库对索引组织表使用称为逻辑rowid的行标识符。逻辑rowid是表主键的base64编码表示形式。逻辑rowid的长度取决于主键的长度。
由于插入操作,索引叶块中的行可能在块内部或块之间移动。索引组织表中的行不会像堆组织行那样迁移(见第12-16页的“链式和迁移行”)。由于索引组织表中的行没有永久的物理地址,数据库使用基于主键的逻辑rowid。
例如,假设departments表是索引组织的。location_id列存储每个部门的ID。表存储如下行,最后一个值为location ID:
10,Administration,200,1700
20,Marketing,201,1800
30,Purchasing,114,1700
40,Human Resources,203,2400
在location_id列上的辅助索引可能有如下索引条目,逗号后面的值为逻辑rowid:
1700,*BAFAJqoCwR/+
1700,*BAFAJqoCwQv+
1800,*BAFAJqoCwRX+
2400,*BAFAJqoCwSn+
辅助索引提供了对索引组织表的快速有效访问,使用的列既不是主键也不是主键的前缀。例如,查询ID大于1700的部门名称可以利用辅助索引加快数据访问。
3.2.3.1. 逻辑Rowids和物理猜测。
辅助索引使用逻辑rowid来定位表行。逻辑rowid包含一个物理猜测,这是最初创建索引项时的物理rowid。Oracle数据库可以使用物理猜测直接探测索引组织表的叶块,绕过主键搜索。当行的物理位置发生变化时,即使包含的物理猜测已经过时,逻辑rowid仍然有效。
对于堆组织表,通过辅助索引访问涉及对辅助索引的扫描和额外的I/O操作来获取包含行的数据块。对于索引组织表,通过辅助索引的访问取决于物理猜测的使用和准确性:
- 没有物理猜测时,访问涉及两次索引扫描:先扫描辅助索引,然后扫描主键索引。
- 有物理猜测时,访问取决于它们的准确性:使用准确的物理猜测时,访问涉及对辅助索引的扫描和额外的I/O操作来获取包含行的数据块。
- 使用不准确的物理猜测时,访问涉及对辅助索引的扫描以及一次I/O操作来获取错误的数据块(由猜测指示),随后是对索引组织表通过主键值进行的唯一性索引扫描。
3.2.3.2. 在索引组织表上创建位图索引
索引组织表上的辅助索引可以是位图索引。如第3-13页“位图索引”中所解释的,位图索引为每个索引键存储一个位图。当索引组织表上存在位图索引时,所有位图索引都使用堆组织的映射表。映射表存储索引组织表的逻辑rowid。每个映射表行存储对应索引组织表行的一个逻辑rowid。
数据库使用搜索键访问位图索引。如果数据库找到该键,则将位图条目转换为物理rowid。对于堆组织表,数据库使用物理rowid访问基表。对于索引组织表,数据库使用物理rowid访问映射表,该映射表反过来产生数据库用于访问索引组织表的逻辑rowid。图3-4说明了对departments_iot表进行查询的索引访问。
注意:索引组织表中的行移动不会使建立在该索引组织表上的位图索引无法使用。