DBMS文章阅读

本文阅读了书籍DBMS的下面的章节
1、Ch7的7.3到7.5小节
2、Ch8的8.1到8.4小节
2、Ch9的9.1到9.7小节

这些章节主要讲解的是数据的存储和管理知识以及B树,下面对这些部分进行详细介绍:

目录

DBMS体系结构

Ch 7

7.3 磁盘空间管理

7.3.1 跟踪空闲块

7.3.2 使用操作系统文件系统管理磁盘空间

7.4 缓冲区管理器

7.4.1 缓冲区替换策略

7.4.2 DBMS与OS中的缓冲区管理

7.5 文件和索引

7.5.1 堆文件

网页链表

页面目录

7.5.2 索引简介

CH 8 文件组织和索引

8.1 成本模型

8.2 三种文件组织的比较

8.2.1 堆文件

 8.2.2 排序文件

8.2.3 散列文件

8.2.4 选择文件组织方式

 8.3 指标概述

8.3.1 索引中数据项的替代方法

 8.4 指标的性质

 8.4.1聚类索引与非聚类索引

 8.4.2 密集索引与稀疏索引

 8.4.3 主索引和二级索引

8.4.4 使用复合搜索键进行索引

9 树形结构索引

9.1 索引顺序访问方法(isam)

9.2  B+树:动态索引结构

9.3 节点的格式

9.4 搜索

 9.5 插入

 9.6 删除

 9.7 副本


DBMS体系结构

 上图为数据库管理系统的结构图,从上到下分别是:

1、用户接口:提供多种不同的接口比如应用界面(客户端web等)、应用程序(cpp/python等)、SQL、管理工具

2、查询处理器:将用户接口传进来的指令转化为低级指令,屏蔽下层的物理实现细节

3、存储管理器:负责将查询处理器的指令转换成(操作系统级别)文件管理系统的指令,保证更好更快的数据查询、更新、存储

4、磁盘存储器:存放有助于数据库运行的数据结构(索引)和实际的数据文件(因为数据很多必须放在磁盘里不能放在内存里)

详细介绍:数据库学习笔记 -----DBMS体系结构介绍_dbms 有关的表结构_HIT_KyleChen的博客-CSDN博客

Ch 7

7.3 磁盘空间管理

DBMS体系结构中的最低级别的软件称为磁盘空间管理器,用于管理磁盘上的空间。磁盘空间管理器支持页面作为数据单元,并提供分配或取消分配页面以及读取或写入页面的命令。页面的大小被选择为磁盘块的大小,页面被存储为磁盘块,因此可以在一个磁盘I/O中读取或写入页面。

将一系列页分配为连续的块序列来保存按顺序频繁访问的数据通常是有用的,这种能力必须由磁盘空间管理器提供给DBMS的更高层。

因此,磁盘空间管理器隐藏了底层硬件(可能还有操作系统)的详细信息,并允许更高级别的软件将数据视为页面的集合。

7.3.1 跟踪空闲块

随着时间的推移,数据库会随着记录的插入和删除而增长和缩小。磁盘空间管理器除了跟踪哪个磁盘块上的页面之外,还跟踪正在使用的磁盘块。尽管最初可能是按顺序在磁盘上分配块,但随后的分配和释放通常会产生“漏洞”。

跟踪块使用情况的一种方法是维护一个空闲块列表。当块被释放时(由请求和使用这些块的高级软件),我们可以将它们添加到空闲列表中以供将来使用。指向空闲块列表中第一个块的指针存储在磁盘上的已知位置。

第二种方法是为每个磁盘块维护一个位图,每个位表示一个块是否正在使用。位图还允许非常快速地识别和分配磁盘上的连续区域。用链表方法很难做到这一点。

7.3.2 使用操作系统文件系统管理磁盘空间

操作系统还管理磁盘空间。通常,操作系统支持将文件抽象为字节序列。操作系统管理磁盘上的空间,并将诸如“读取文件f的字节i”之类的请求转换为相应的低级指令:“读取磁盘d的圆柱体c的磁道t的块m”。可以使用操作系统文件构建数据库磁盘空间管理器。例如,整个数据库可以驻留在一个或多个操作系统文件中,操作系统为这些文件分配并初始化了许多块。磁盘空间管理器负责管理这些操作系统文件中的空间。

许多数据库系统不依赖于操作系统文件系统,而是自己进行磁盘管理,或者从零开始,或者通过扩展操作系统的功能。原因既有技术上的,也有实际的。一个实际的原因是,为了可移植性,希望支持多个操作系统平台的DBMS供应商不能假设特定于任何操作系统的特性,因此会尝试使DBMS代码尽可能自包含。技术上的原因是,在32位系统上,最大的文件大小为4 GB,而DBMS可能希望访问比这更大的单个文件。一个相关的问题是,典型的操作系统文件不能跨越磁盘设备,这在DBMS中通常是可取的,甚至是必要的。数据库管理系统不依赖于操作系统文件系统的其他技术原因将在7.4.2节中列出。

7.4 缓冲区管理器

为了理解缓冲区管理器的作用,考虑一个简单的例子。假设数据库包含1,000,000页,但是只有1,000页的主存可用于保存数据。考虑一个需要扫描整个文件的查询。由于不能一次将所有数据都放入主存,因此DBMS必须在需要时将页放入主存,并在此过程中决定要替换主存中的哪些现有页,以便为新页腾出空间。用于决定替换哪个页面的策略称为替换策略。

根据第1.8节介绍的DBMS体系结构,缓冲区管理器是负责根据需要将页面从磁盘带到主存的软件层。缓冲管理器通过将可用的主内存划分为一组页面来管理它,我们将这些页面统称为缓冲池。缓冲池中的主内存页称为帧;可以方便地将它们看作可以容纳页面的插槽(通常驻留在磁盘或其他辅助存储介质上)。

可以编写更高级别的DBMS代码,而不必担心数据页是否在内存中;它们向缓冲管理器请求该页,如果该页不存在,则该页将被带入缓冲池中的一个框架中。当然,请求页的高级代码也必须在不再需要该页时释放该页,通过通知缓冲区管理器,以便包含该页的框架可以被重用。高级代码还必须通知缓冲区管理器,如果它修改了所请求的页;然后,缓冲区管理器确保将更改传播到磁盘上的页面副本。缓冲区管理如图7.3所示。

 除了缓冲池本身,缓冲管理器还维护一些簿记信息,以及池中每个帧的两个变量:pin_count(引脚计数)和dirty。给定帧中当前页面被请求但未被释放的次数——页面当前用户的数量——记录在该帧的pin_count变量中。布尔变量dirty表示该页从磁盘进入缓冲池后是否被修改过。

最初,每帧的引脚数设置为0,并且关闭dirty位。当请求页面时,缓冲区管理器执行以下操作:

1. 检查缓冲池,看某个帧是否包含请求的页面,如果包含,则增加该帧的pin_count。如果页不在池中,缓冲管理器按以下方式将其引入:

(a)使用替换策略选择要替换的帧,并增加其pin_count。
(b)如果替换帧的dirty位打开,则将该页包含的页写入磁盘(即该页的磁盘副本被该帧的内容覆盖)。
(c)将请求的页面读入替换框架。

2. 将包含被请求页面的帧的(主存)地址返回给请求者。

增加pin_count通常称为将请求的页面固定在其帧中。当调用缓冲区管理器并请求该页的代码随后调用缓冲区管理器并释放该页时,包含所请求页的帧的pin_count将递减。这被称为解除页面的固定。如果请求者修改了页面,它也会在解除页面固定时通知缓冲区管理器,并设置帧的dirty位。缓冲区管理器不会将另一个页读入帧中,直到其pin_count变为0,也就是说,直到该页的所有请求者都解除了该页的pin_count。

如果请求的页面不在缓冲池中,并且缓冲池中没有可用的帧,则选择pin_count为0的帧进行替换。如果有许多这样的帧,则根据缓冲区管理器的替换策略选择一个帧。我们将在7.4.1节讨论各种替换策略。

当最终选择一个页进行替换时,如果dirty位没有设置,则意味着该页自从被放入主存以来没有被修改过。因此,不需要将页面写回磁盘;磁盘上的副本与帧中的副本是相同的,并且可以简单地用新请求的页面覆盖帧。否则,必须将对该页的修改传播到磁盘上的副本。(崩溃恢复协议可能会施加进一步的限制,正如我们在1.7节看到的那样。例如,在预写日志(Write-Ahead Log, WAL)协议中,特殊的日志记录用于描述对页面所做的更改。与要替换的页面相关的日志记录可能在缓冲区中;如果是这样,协议要求在将页写入磁盘之前将它们写入磁盘。)

如果缓冲池中没有pin_count为0的页面,并且请求了一个不在池中的页面,缓冲管理器必须等待,直到某个页面被释放,然后才响应该页请求。实际上,在这种情况下,请求页面的事务可能会被中止!因此,应该尽快释放页面——由调用缓冲区管理器请求页面的代码释放。

此时一个很好的问题是“如果一个页面被几个不同的事务请求怎么办?”也就是说,如果页面是由代表不同用户独立执行的程序请求的呢?这类程序有可能对页面进行相互冲突的更改。锁定协议(由高级DBMS代码强制执行,特别是事务管理器)确保每个事务在请求读取或修改页面之前获得共享或排他锁。两个不同的事务不能同时在同一页上持有排他锁;这就是防止相互冲突的变化的方法。缓冲区管理器只是假设在请求页面之前已经获得了适当的锁。

7.4.1 缓冲区替换策略

用于选择要替换的未固定页面的策略可以在很大程度上影响数据库操作所花费的时间。存在许多备选策略,每种策略都适用于不同的情况。

最著名的替换策略是最近最少使用(LRU)。这可以在缓冲区管理器中使用一个指向引脚计数为0的帧的指针队列来实现。当帧成为替换的候选帧时(即,当引脚计数变为0时),将帧添加到队列的末尾。选择替换的页面是位于队列头部的帧中的页面。

LRU的一种变体,称为时钟替换,具有类似的行为,但开销更小。其思想是使用取值为1到N的当前变量选择要替换的页面,其中N是缓冲帧的数量,按照循环顺序。我们可以把框架想象成一个圆圈,就像时钟的表盘,电流就像时钟的指针在表盘上移动。为了近似LRU行为,每个帧也有一个相关的引用位,当页面引脚计数变为0时,它被打开。

考虑更换当前框架。如果没有选择替换帧,则电流增加,并考虑下一帧;这个过程一直重复,直到选定了某个帧。如果当前帧的引脚数大于0,则它不是替换的候选帧,并且电流增加。如果当前帧的引用位打开,则时钟算法关闭引用位并增加电流-这样,最近引用的页面不太可能被替换。如果当前帧的引脚计数为0,并且其引用位关闭,则选择其中的页面进行替换。如果所有帧都固定在时钟指针的某个扫描中(也就是说,current的值不断增加,直到它重复),这意味着缓冲池中没有页面是替代候选页。

LRU和时钟策略并不总是数据库系统的最佳替代策略,特别是在许多用户请求需要对数据进行顺序扫描的情况下。考虑以下说明性情况。假设缓冲池有10个帧,而要扫描的文件只有10个或更少的页面。为简单起见,假设没有对页面的竞争请求,那么只有文件的第一次扫描进行任何I/O操作。后续扫描中的页面请求总是会在缓冲池中找到所需的页面。另一方面,假设要扫描的文件有11个页面(比缓冲池中可用页面的数量多一个)。使用LRU,每次扫描文件都会读取文件的每个页面!在这种情况下,称为顺序泛洪,LRU是最糟糕的替换策略。

实际的缓冲区管理:IBM DB2和Sybase ASE允许将缓冲区划分为命名池。每个数据库、表或索引都可以绑定到其中一个池。每个池可以配置为在ASE中使用LRU或时钟替换;DB2使用时钟替换的一种变体,初始时钟值基于页面的性质(例如,索引非叶子获得更高的起始时钟值,这会延迟它们的替换)。有趣的是,DB2中的缓冲池客户机可以显式地指示它讨厌某个页面,从而使该页成为下一个要替换的选择。作为一种特殊情况,DB2对某些实用程序操作(例如RUNSTATS)中获取的页面应用MRU, DB2 V6还支持FIFO。Informix和Oracle 7都使用LRU维护一个全局缓冲池;Microsoft SQL Server有一个使用时钟替换的单池。在Oracle 8中,表可以绑定到两个池之一;一个具有高优先级,并且系统试图将此池中的页面保留在内存中。

除了为给定事务设置最大引脚数之外,通常没有用于控制每个事务的缓冲池使用情况的特性。然而,Microsoft SQL Server支持通过需要大量内存的查询(例如,涉及排序或哈希的查询)来保留缓冲区页面。

其他替换策略包括先进先出(FIFO)和最近使用(MRU),它们也需要类似于LRU的开销,以及随机等。这些策略的细节应该从它们的名称和前面对LRU和时钟的讨论中显而易见。

7.4.2 DBMS与OS中的缓冲区管理

操作系统中的虚拟内存和数据库管理系统中的缓冲区管理之间存在明显的相似之处。在这两种情况下,目标都是提供对比主存更大的数据的访问,基本思想是根据需要将页从磁盘引入主存,替换主存中不再需要的页。为什么我们不能使用操作系统的虚拟内存能力来构建DBMS ?DBMS通常可以预测页面被访问的顺序或页面引用模式,比OS环境中的典型预测要准确得多,并且希望利用这一属性。此外,DBMS需要比操作系统通常提供的更多地控制何时将页写入磁盘。

DBMS通常可以预测引用模式,因为大多数页面引用都是由具有已知页面访问模式的高级操作(例如顺序扫描或各种关系代数运算符的特定实现)生成的。这种预测引用模式的能力允许更好地选择要替换的页面,并使专用缓冲区替换策略的想法在DBMS环境中更具吸引力。

预取:在IBM DB2中,既支持顺序预取,也支持列表预取(预取页面列表)。通常,预取大小为32个4KB页面,但这可以由用户设置。对于一些顺序类型的数据库实用程序(例如COPY、RUNSTATS), DB2将预取最多64个4KB的页面。对于较小的缓冲池(即小于1000个缓冲区),预取数量向下调整为16或8页。预取大小可由用户配置;对于某些环境,一次预取1000页可能是最好的!Sybase ASE支持最多256页的异步预取,并使用此功能减少在范围扫描中对表进行索引访问期间的延迟。Oracle 8对顺序扫描、检索大对象和某些索引扫描使用预取。Microsoft SQL Server支持顺序扫描和沿着B+树索引的叶子级扫描的预取,并且可以随着扫描的进行调整预取的大小。SQL Server也广泛使用异步预取。Informix支持使用用户定义的预取大小进行预取。

更重要的是,能够预测引用模式可以使用一种简单而非常有效的策略,称为页面预取。缓冲区管理器可以预测接下来的几个页面请求,并在请求页面之前将相应的页面获取到内存中。这种策略有两个好处。首先,当请求页面时,它们在缓冲池中可用。其次,在连续的页面块中读取要比在不同时间读取相同的页面以响应不同的请求快得多。(回顾磁盘几何的讨论,了解为什么会这样。)如果要预取的页面不是连续的,那么认识到需要提取多个页面仍然可以带来更快的I/O,因为可以为这些页面选择检索顺序,从而最大限度地减少寻道时间和旋转延迟。

顺便提一下,请注意I/O通常可以与CPU计算并发进行。一旦向磁盘发出预取请求,磁盘负责将请求的页面读入内存页面,CPU可以继续执行其他工作。

DBMS还需要显式地将页面强制到磁盘上的能力,也就是说,要确保磁盘上的页面副本使用内存中的副本进行更新。与此相关的一点是,DBMS必须能够确保缓冲池中的某些页面在写入其他页面之前被写入磁盘,以便实现用于崩溃恢复的WAL协议,正如我们在1.7节中看到的那样。操作系统中的虚拟内存实现不能依赖于提供对何时将页写入磁盘的控制;将页写入磁盘的操作系统命令可以通过记录写请求来实现,并推迟对磁盘副本的实际修改。如果系统在此期间崩溃,对DBMS的影响可能是灾难性的。(崩溃恢复将在第20章进一步讨论。)

7.5 文件和索引

现在,我们将注意力从页面存储在磁盘上并进入主存的方式转向页面用于存储记录并组织成逻辑集合或文件的方式。更高级别的DBMS代码将页面视为有效的记录集合,忽略了表示和存储细节。事实上,记录集合的概念并不局限于单个页面的内容;记录文件是记录的集合,可以驻留在几个页面上。在本节中,我们将考虑如何将页面集合组织为一个文件。我们将在7.6节和7.7节讨论如何组织页面上的空间来存储记录集合。

每条记录都有一个唯一的标识符,称为record id,或简称rid。正如我们将在7.6节中看到的,我们可以通过使用记录的rid来识别包含记录的页面。我们考虑的基本文件结构称为堆文件,它以随机顺序存储记录,并支持检索所有记录或检索由其rid指定的特定记录。有时,我们希望通过在所需记录的字段上指定某些条件来检索记录,例如,“查找年龄为35岁的所有员工记录”。为了加快这种选择,我们可以构建辅助数据结构,使我们能够快速找到满足给定选择条件的员工记录。这种辅助结构称为索引;我们将在7.5.2节中介绍索引。

7.5.1 堆文件

最简单的文件结构是无序文件或堆文件。堆文件页面中的数据没有以任何方式排序,唯一的保证是可以通过重复请求下一条记录来检索文件中的所有记录。文件中的每条记录都有一个唯一的rid,文件中的每一页都有相同的大小。

支持的堆文件操作包括创建和销毁文件、插入记录、删除具有给定rid的记录、获取具有给定rid的记录以及扫描文件中的所有记录。要获取或删除具有给定rid的记录,请注意,在给定记录id的情况下,我们必须能够找到包含该记录的页面的id。

为了支持扫描,我们必须跟踪每个堆文件中的页面,为了有效地实现插入,我们必须跟踪包含空闲空间的页面。我们将讨论维护此信息的两种替代方法。在这些备选方案中,除了保存数据外,页还必须保存两个指针(即页id),用于文件级簿记。

网页链表

一种可能性是将堆文件维护为双重链接的页面列表。数据库管理系统可以记住第一页的位置,通过维护一个表,其中包含对堆文件名,页1地址?在磁盘上的已知位置。我们称文件的第一页为头页。

一个重要的任务是维护通过从堆文件中删除一条记录而创建的空槽的信息。这个任务有两个不同的部分:如何跟踪页面内的空闲空间,以及如何跟踪有空闲空间的页面。我们在7.6节中考虑第一部分。第二部分可以通过保持一个有空闲空间的页面的双链表和一个完整页面的双链表来解决;这些列表一起包含堆文件中的所有页面。这种组织如图7.4所示;注意,每个指针实际上是一个页id。

 如果需要一个新页,则通过向磁盘空间管理器发出请求来获得它,然后将其添加到文件中的页列表中(可能作为具有空闲空间的页,因为新记录不太可能占用该页上的所有空间)。如果要从堆文件中删除一个页面,它将从列表中删除,并通知磁盘空间管理器释放该页面。(请注意,该方案可以很容易地推广到分配或释放多个页面序列,并维护这些页面序列的双重链接列表。)

这种方案的一个缺点是,如果记录的长度是可变的,那么实际上文件中的所有页面都将在空闲列表中,因为很可能每个页面至少有几个空闲字节。要插入一条典型的记录,我们必须检索并检查空闲列表中的多个页面,然后才能找到一个有足够空闲空间的页面。我们接下来讨论的基于目录的堆文件组织解决了这个问题。

页面目录

页面链表的另一种替代方法是维护页面目录。DBMS必须记住每个堆文件的第一个目录页的位置。目录本身是一个页面集合,在图7.5中显示为一个链表。(当然,其他组织也可以用于目录本身。)

 每个目录条目标识堆文件中的一个页(或一系列页)。随着堆文件的增加或减少,目录中的条目数量(可能还有目录本身的页面数量)也相应增加或减少。注意,由于与典型页面相比,每个目录条目都非常小,因此与堆文件的大小相比,目录的大小可能非常小。
可以通过维护每个条目一个比特来管理空闲空间,这表示相应的页面是否有任何空闲空间,或者通过每个条目计数来表示页面上的空闲空间量。如果文件包含可变长度的记录,我们可以检查条目的空闲空间计数,以确定该记录是否适合该条目所指向的页面。由于一个目录页可以容纳多个条目,因此我们可以高效地搜索具有足够空间的数据页,以容纳要插入的记录。

7.5.2 索引简介

有时,我们希望找到在特定字段中具有给定值的所有记录。如果我们能找到所有这些记录的rid,我们就能从记录的rid中找到包含每条记录的页面;然而,堆文件组织并不能帮助我们找到这些记录的rid。索引是一种辅助数据结构,用于帮助我们找到满足选择条件的记录。

考虑一下如何在图书馆找到你想要的书。您可以搜索索引卡的集合,按作者姓名或书名排序,以查找图书的呼号。因为书是根据借阅号码存储的,借阅号码使您能够走到包含您需要的书的书架。注意,作者姓名的索引不能用于根据书名定位图书,反之亦然;每个索引都加快了某些类型的搜索,但不是全部。图7.6说明了这一点。

 当我们希望支持对文件中所需数据子集的有效检索时,同样的思想也适用。从实现的角度来看,索引只是另一种类型的文件,包含对数据记录请求进行流量指导的记录。每个索引都有一个关联的搜索键,它是我们正在构建索引的记录文件的一个或多个字段的集合;字段的任何子集都可以作为搜索关键字。我们有时把记录文件称为索引文件。

索引旨在加快搜索键上的相等或范围选择。例如,如果我们想建立一个索引来提高查询给定年龄的雇员的效率,我们可以在雇员数据集的年龄属性上建立一个索引。存储在索引文件中的记录(我们将其称为条目,以避免与数据记录混淆)允许我们使用给定的搜索键值查找数据记录。在我们的示例中,索引可能包含?age, rid ?对,其中rid标识数据记录。

索引文件中的页面以某种方式组织,使我们能够快速定位索引中具有给定搜索键值的条目。例如,我们必须查找年龄≥30的条目(然后按照检索条目中的rid),以便查找年龄超过30岁的员工记录。组织技术,或索引文件的数据结构被称为访问方法,其中有几个是已知的,包括B+树(第9章)和基于哈希的结构(第10章)。B+树索引文件和基于散列的索引文件是使用磁盘空间管理器提供的页面分配和操作工具构建的,就像堆文件一样。

商业系统中的rid: IBM DB2、Informix、Microsoft SQL Server、Oracle 8和Sybase ASE都将记录id实现为页id和槽号。Sybase ASE使用以下页面组织,这是典型的:页面包含一个标头,后跟行和一个槽数组。页头包含页标识、其分配状态、页空闲空间状态和时间戳。槽数组只是槽号到页偏移量的映射。Oracle 8和SQL Server在一种特殊情况下使用逻辑记录id,而不是页id和槽号:如果一个表有聚集索引,那么表中的记录将使用聚集索引的键值来标识。这样做的好处是,如果记录跨页面移动,则不需要重新组织二级索引。

CH 8 文件组织和索引

文件组织是当文件存储在磁盘上时安排文件中记录的一种方式。一个记录文件可能以多种方式被访问和修改,排列记录的不同方式可以有效地对文件执行不同的操作。例如,如果我们希望按字母顺序检索员工记录,那么按名称对文件进行排序是一种很好的文件组织方式。另一方面,如果我们想要检索工资在给定范围内的所有员工,那么按姓名对员工记录排序并不是一个好的文件组织方式。DBMS支持多种文件组织技术,DBA的一项重要任务是根据预期的使用模式为每个文件选择良好的组织方式。

我们以本书中使用的成本模型8.1节中的讨论开始本章。在8.2节中,我们对三种基本的文件组织进行了简化分析:随机排序记录的文件(即堆文件),按某些字段排序的文件,以及按某些字段散列的文件。我们的目标是强调选择适当的文件组织的重要性。

每个文件组织都使某些操作变得高效,但我们通常希望支持多个操作。例如,在name字段上对员工记录的文件进行排序可以方便地按字母顺序检索员工,但是我们可能还希望检索55岁以上的所有员工;为此,我们必须扫描整个文件。为了处理这种情况,DBMS建立一个索引,我们将在第7.5.2节中描述。文件上的索引被设计用来加快该文件中记录的基本组织不能有效支持的操作。后面的章节介绍了几个特定的索引数据结构;在本章中,我们将重点讨论不依赖于所使用的特定索引数据结构的索引属性。

第8.3节介绍了索引作为一种通用技术,它可以加快检索具有给定值的记录的速度。第8.4节讨论索引的一些重要属性,第8.5节讨论创建索引的DBMS命令。

8.1 成本模型

在本节中,我们将介绍一个成本模型,它允许我们估计不同数据库操作的成本(按执行时间计算)。我们将在分析中使用以下符号和假设。有B个数据页,每个页有R条记录。

读取或写入磁盘页面的平均时间为D,处理一条记录(例如,将字段值与选择常数进行比较)的平均时间为C。在哈希文件组织中,我们将使用一个称为哈希函数的函数将一条记录映射到一个数字范围;将哈希函数应用于记录所需的时间为H。

今天的典型值是D = 15毫秒,C和H = 100纳秒;因此,我们预计I/O成本将占主导地位。这个结论得到了当前硬件趋势的支持,CPU速度在稳步上升,而磁盘速度却没有以类似的速度增长。另一方面,随着主内存大小的增加,所需页面的更大一部分可能会适合内存,从而导致更少的I/O请求。

因此,我们在本书中使用磁盘页面I/O的数量作为我们的成本指标。

  • 我们强调,实际系统必须考虑成本的其他方面,例如CPU成本(以及分布式数据库中的传输成本)。然而,我们的目标主要是介绍底层算法,并说明如何估计成本。因此,为了简单起见,我们选择只关注成本的I/O部分。考虑到I/O通常(甚至通常)是数据库操作成本的主要组成部分,考虑I/O成本可以让我们很好地初步估计真实成本。
  • 即使我们决定关注I/O成本,对于我们以简单的方式传达基本思想的目的来说,精确的模型也过于复杂。因此,我们选择使用一个简单的模型,在这个模型中,我们只计算从磁盘读取或写入磁盘的页面数量,作为I/O的度量。我们忽略了阻塞访问的重要问题——通常,磁盘系统允许我们在单个I/O请求中读取连续页面块。该代价等于查找块中的第一个页面并传输块中所有页面所需的时间。这种阻塞访问比块中每个页面发出一个I/O请求要便宜得多,特别是如果这些请求不是连续的:我们将为块中的每个页面增加额外的寻道成本。

当我们在本章和后面的章节中讨论各种算法的成本时,必须记住我们所选择的成本度量的讨论。每当我们简化的假设可能以重要的方式影响我们从分析中得出的结论时,我们就会讨论成本模型的含义。

8.2 三种文件组织的比较

现在我们比较三种基本文件组织的一些简单操作的成本:随机顺序记录的文件,或堆文件;按字段序列排序的文件;以及按字段序列散列的文件。对于排序和散列的文件,对文件进行排序或散列的字段序列(例如,工资、年龄)称为搜索键。注意,索引的搜索键可以是一个或多个字段的任意序列;它不需要唯一地标识记录。我们注意到,在数据库文献中有一个令人遗憾的术语键的过载。主键或候选键(唯一标识一条记录的字段;参见第3章)与搜索键的概念无关。

我们的目标是强调选择合适的文件组织是多么重要。我们考虑的操作如下所述。

  • 扫描: 获取文件中的所有记录。文件中的页必须从磁盘提取到缓冲池中。在页面(池中)定位记录时,每条记录也有CPU开销。
  • 使用相等性选择进行搜索: 获取满足相等性选择的所有记录,例如,“查找sid为23的学生的Students记录”。包含合格记录的页必须从磁盘中获取,并且合格记录必须位于检索到的页中。
  • 使用范围选择进行搜索: 获取满足范围选择的所有记录,例如,“查找姓名按字母顺序排在' Smith '后面的所有Students记录”。
  • 插入: 将给定的记录插入到文件中。我们必须在文件中确定新记录必须插入的页面,从磁盘中取出该页,修改它以包含新记录,然后回写修改后的页面。根据文件组织,我们可能还需要获取、修改和回写其他页面。
  • 删除: 删除使用rid指定的记录。我们必须识别包含记录的页面,从磁盘中获取记录,修改记录,并将其写回。根据文件组织,我们可能还需要获取、修改和回写其他页面。

8.2.1 堆文件

扫描: 成本是B(D + RC),因为我们必须花费时间检索每个B页的每个页面D,对于每个页面,进程R记录每个记录花费的时间C。

用相等选择搜索: 假设我们事先知道正好有一个记录匹配所需的相等性选择,即在候选关键字。平均而言,我们必须扫描一半的文件,假设记录存在并且值在搜索字段中的分布是均匀的。对于每个检索到的数据页,我们必须检查该页上的所有记录,以确定它是否是所需的记录。成本为0.5B(D + RC)。但是,如果没有满足选择的记录,则必须扫描整个文件以验证这一点。

如果选择不在候选关键字段上(例如,“Find students aged 18”),我们总是必须扫描整个文件,因为age = 18的几个记录可能分散在整个文件中,而我们不知道有多少这样的记录存在。

使用范围选择进行搜索: 必须扫描整个文件,因为符合条件的记录可能出现在文件中的任何位置,而我们不知道有多少符合条件的记录存在。成本是B(D + RC)。

插入: 我们假设记录总是在文件的末尾插入。我们必须获取文件中的最后一页,添加记录,并将该页写回。代价是2D + C。

删除: 我们必须找到记录,从页面中删除记录,并将修改后的页面写回来。为简单起见,我们假设没有尝试压缩文件以回收删除所创建的空闲空间。代价是搜索的代价加上C + D。

我们假设要删除的记录是使用记录id指定的。由于页id可以很容易地从记录id中获得,因此我们可以直接读入该页。因此,搜索的代价是D。

如果要删除的记录是在某些字段上使用相等或范围条件指定的,则在讨论相等和范围选择时给出了搜索成本。删除成本还受到合格记录数量的影响,因为必须修改包含此类记录的所有页面。

 8.2.2 排序文件

扫描: 成本是B(D + RC),因为所有页面都必须检查。请注意,没有比无序文件更好或更坏的情况。然而,其中的顺序检索的记录对应于排序顺序。

使用相等选择进行搜索: 我们假设指定了相等选择对文件进行排序的字段;如果不是,则开销与堆文件的开销相同。我们可以找到包含所需记录的第一页,如果存在任何符合条件的记录,则使用log 2b步骤中的二进制搜索。(此分析假设已排序文件中的页面按顺序存储,并且我们可以在一个磁盘I/O中直接检索文件上的第I页。例如,如果排序文件是作为使用链表组织的堆文件实现的,并且页面以适当的排序顺序排列,则此假设无效。)每一步都需要一次磁盘I/O和两次比较。一旦知道了该页,就可以再次通过对该页进行二进制搜索来找到第一个符合条件的记录,成本为Clog 2r,成本为Clog 2b + Clog 2r,这比搜索堆文件有了很大的改进。

如果有几个符合条件的记录(例如,“查找所有年龄为18岁的学生”),由于年龄排序,它们保证彼此相邻,因此检索所有此类记录的成本等于定位第一个此类记录的成本(log 2b +Clog 2r)加上按顺序读取所有符合条件的记录的成本。通常,所有符合条件的记录都放在一页上。如果没有符合条件的记录,则通过搜索第一个符合条件的记录来建立,该记录查找包含符合条件记录的页面(如果存在的话),并搜索该页。

使用范围选择进行搜索: 再次假设范围选择位于排序字段上,那么满足选择的第一个记录的位置与使用相等搜索时的位置相同。随后,依次检索数据页,直到找到不满足范围选择的记录;这类似于具有许多符合条件记录的相等性搜索。

成本是搜索成本加上检索满足搜索的记录集的成本。搜索的成本包括获取包含合格或匹配记录的第一页的成本。对于小范围选择,所有符合条件的记录都会显示在此页上。对于更大范围的选择,我们必须获取包含匹配记录的额外页面。

插入: 要在保留排序顺序的情况下插入一条记录,我们必须首先在文件中找到正确的位置,添加记录,然后获取并重写所有后续页面(因为所有旧记录将被移动一个槽,假设文件没有空槽)。一般来说,我们可以假设插入的记录位于文件的中间。因此,我们必须读取文件的后半部分,然后在添加新记录后将其写回。因此,代价是查找新记录位置的代价加上2 * (0.5B(D + RC)),即搜索代价加上B(D + RC)。

删除: 我们必须搜索记录,从页面中删除记录,并将修改后的页面写回来。我们还必须读写所有后续页,因为必须将删除记录后面的所有记录向上移动以压缩可用空间。代价与插入相同,即搜索代价加上B(D + RC)。给定要删除的记录,我们可以直接获取包含该记录的页面。

如果要删除的记录是由相等或范围条件指定的,则删除的成本取决于符合条件的记录的数量。如果在排序字段上指定了条件,则由于排序,可以保证符合条件的记录是连续的,并且可以使用二进制搜索找到第一个符合条件的记录。

8.2.3 散列文件

简单的散列文件组织使我们能够快速定位具有给定搜索键值的记录,例如,“查找Joe的Students记录”,如果文件在name字段上进行了散列。

散列文件中的页面被分组到桶中。给定一个桶号,散列文件结构允许我们找到该桶的主页。记录所属的桶可以通过对搜索字段应用一个称为哈希函数的特殊函数来确定。在插入时,将一条记录插入到适当的桶中,如果桶的主页满了,就会分配额外的“溢出”页。每个桶的溢出页都保存在一个链表中。要搜索具有给定搜索键值的记录,我们只需应用散列函数来标识这些记录所属的存储桶,并查看该存储桶中的所有页面。

这种组织称为静态散列文件,它的主要缺点是可能会形成长链的溢出页。这可能会影响性能,因为必须搜索bucket中的所有页面。解决这个问题的动态哈希结构是已知的,我们将在第10章讨论它们;对于本章的分析,我们将简单地假设没有溢出页。

扫描: 在散列文件中,页面的占用率保持在80%左右(为将来的插入留出一些空间,并在文件扩展时尽量减少溢出页面)。这是通过在每个现有页面满80%时向桶中添加一个新页面来实现的,当记录最初被组织成散列文件结构时。因此,页面数和扫描所有数据页面的成本大约是扫描一个无序文件的成本的1.25倍,即1.25 b (D + RC)。

使用相等选择进行搜索: 如果选择在散列文件的搜索键上,则非常有效地支持此操作。(否则,必须扫描整个文件。)识别包含合格记录的页面的成本是H;假设这个桶只包含一个页面(即没有溢出页面),检索它的成本为D。如果我们假设在扫描页面上的一半记录后找到记录,则成本为H + D + 0.5RC。这甚至比排序文件的成本更低。如果有几个符合条件的记录,或者没有,我们仍然只需要检索一个页面,但是我们必须扫描整个页面。

请注意,与散列文件相关的散列函数将记录映射到基于所有搜索关键字段的值的桶;如果没有指定这些字段中的任何一个的值,我们就无法判断记录属于哪个桶。因此,如果选择不是所有搜索关键字段的相等条件,我们必须扫描整个文件。

使用范围选择进行搜索: 哈希结构没有提供帮助;即使范围选择在搜索键上,也必须扫描整个文件。成本为1.25B(D + RC)。

插入: 必须找到、修改适当的页面,然后写回。代价是搜索的代价加上C + D。

删除: 我们必须搜索记录,将其从页面中删除,并将修改后的页面写回来。成本也是搜索成本加上C + D(编写修改后的页面)。

如果使用搜索键上的相等条件指定要删除的记录,则保证所有符合条件的记录都在同一桶中,可以通过应用散列函数来标识桶。

8.2.4 选择文件组织方式

图8.1 比较了三种文件组织的I/O成本。堆文件具有良好的存储效率,支持对记录的快速扫描、插入和删除。然而,它的搜索速度很慢。

排序文件也提供了很好的存储效率,但是插入和删除记录的速度很慢。它的搜索速度非常快,并且是范围选择的最佳结构。值得注意的是,在真正的DBMS中,文件几乎从来没有完全排序过。我们将在第9章讨论一种叫做B+树的结构,它提供了排序文件的所有优点,并有效地支持插入和删除。(相对于已排序的文件,这些好处会带来空间开销,但这种权衡是值得的。)

文件有时保持“几乎排序”,因为它们最初是排序的,每个页面上留有一些空闲空间以容纳未来的插入,但是一旦使用了这个空间,就会使用溢出页来处理插入。插入和删除的成本与堆文件相似,但排序的程度会随着文件的增长而恶化。

散列文件不像排序文件那样充分利用空间,但是插入和删除非常快,相等选择非常快。然而,该结构不支持范围选择,并且完整文件扫描稍微慢一些;空间利用率越低意味着文件包含的页面越多。

总而言之,图8.1表明,没有一个文件组织在所有情况下都是统一的优越。如果只需要完整的文件扫描,无序文件是最好的。如果最常见的操作是相等选择,那么散列文件是最好的。如果需要范围选择,最好使用排序文件。我们在这里研究的组织可以得到改进——静态散列中的溢出页问题可以通过使用动态散列结构来解决,在排序文件中插入和删除的高成本可以通过使用树结构索引来解决——但是,选择适当的文件组织取决于文件的常用方式这一主要观察结果仍然有效。

 8.3 指标概述

正如我们前面提到的,文件上的索引是一种辅助结构,旨在加快该文件中记录的基本组织不能有效支持的操作。

索引可以看作是数据条目的集合,用一种有效的方法来定位所有搜索键值为k的数据条目。每个这样的数据条目,我们用k *表示,包含足够的信息,使我们能够检索(一个或多个)搜索键值为k的数据记录。(注意,数据条目通常不同于数据记录!) 图8.2显示了一个搜索键为sal的索引,其中包含<sal, rid>对作为数据条目。此索引中数据条目的rid组件是指向搜索键值为sal的记录的指针。

需要考虑的两个重要问题是:

  • 如何组织数据条目以支持具有给定搜索键值的数据条目的有效检索?
  • 究竟什么是作为数据条目存储的?

组织数据条目的一种方法是对搜索键上的数据条目进行散列。在这种方法中,我们基本上将数据条目的集合视为一个记录文件,根据搜索键进行散列。这就是图8.2所示的sal上的索引的组织方式。这个例子中的哈希函数h非常简单;它将搜索键值转换为二进制表示,并使用两个最低有效位作为桶标识符。组织数据条目的另一种方法是构建一个指导搜索数据条目的数据结构。已知有几种索引数据结构允许我们有效地查找具有给定搜索键值的数据项。我们将在第9章学习基于树的索引结构,在第10章学习基于哈希的索引结构。

在下一节中,我们将考虑数据项中存储的内容。

8.3.1 索引中数据项的替代方法

数据项k *允许我们检索一个或多个键值为k的数据记录。我们需要考虑三个主要的替代方案:
1. 数据项k *是一个实际的数据记录(搜索键值为k)。
2. 数据项是<k, rid >其中rid是搜索键值为k的数据记录的记录id。
3.数据项是<k, rid-list >其中rid-list是搜索键值为k的数据记录的记录id列表。

注意,如果索引使用Alternative(1),则除了索引的内容外,不需要单独存储数据记录。我们可以把这样的索引看作是一种特殊的文件组织,可以用来代替排序文件或堆文件组织。图8.2演示了Alternatives(1)和(2)。员工记录文件按年龄散列;我们可以将其视为一个索引结构,其中对age值应用哈希函数以定位记录的桶,并对数据条目使用Alternative(1)。sal上的索引还使用散列来定位数据条目,这些条目现在是<sal;rid of employee record>对,也就是,Alternative(2)用于数据条目。

Alternative(2)和(3)包含指向数据记录的数据条目,它们独立于用于索引文件(即包含数据记录的文件)的文件组织。Alternative(3)比Alternative(2)提供了更好的空间利用率,但是数据条目的长度是可变的,这取决于具有给定搜索键值的数据记录的数量。

如果我们希望在一组数据记录上构建多个索引,例如,我们希望在age和sal字段上构建索引,如图8.2所示,那么其中最多应该有一个索引使用Alternative(1),因为我们希望避免多次存储数据记录。

我们注意到,用于加速对具有给定搜索键的数据条目的搜索的不同索引数据结构可以与数据条目的三种替代方案中的任何一种组合使用。 

 8.4 指标的性质

在本节中,我们将讨论影响使用索引进行搜索效率的索引的一些重要属性。

 8.4.1聚类索引与非聚类索引

当一个文件被组织成数据记录的顺序与某个索引中数据条目的顺序相同或接近时,我们称该索引为聚类索引。根据定义,使用Alternative(1)的索引是聚类的。使用Alternative(2)或Alternative(3)的索引只有在数据记录按搜索关键字字段排序时才能成为聚类索引。否则,数据记录的顺序是随机的,纯粹由它们的物理顺序来定义,并且没有合理的方法来按照相同的顺序排列索引中的数据条目。(基于散列的索引不按搜索键排序存储数据项,因此散列索引只有在使用Alternative(1)时才聚类。)

按照搜索键的排序顺序维护数据项的索引使用一组索引项(组织成树结构)来指导对数据项的搜索,这些数据项按排序顺序存储在树的叶级。聚类和非聚类树索引如图8.3和8.4所示;我们将在第9章进一步讨论树结构索引。为了简单起见,在图8.3中,我们假设数据记录的底层文件是完全排序的。

 

 在实践中,很少按照完全排序的顺序维护数据记录,除非使用Alternative(1)将数据记录存储在索引中,因为在插入和删除记录时移动数据记录以保持排序顺序的开销很高。通常情况下,记录最初是排序的,并且每个页面都留下一些可用空间来吸收未来的插入。如果页上的可用空间随后被用完(在初始排序步骤之后插入的记录),则使用溢出页的链表处理对该页的进一步插入。因此,过了一段时间后,记录的顺序仅近似于预期的排序顺序,并且必须重新组织文件(即重新排序)以确保良好的性能。

因此,在更新文件时,聚类索引的维护成本相对较高。聚类索引维护成本高的另一个原因是,数据条目可能必须跨页移动,如果记录是由页id和槽的组合标识的,通常情况下,数据库中指向移动记录的所有位置(通常是同一记录集合的其他索引中的条目)也必须更新以指向新的位置;这些额外的更新可能非常耗时。

一个数据文件最多可以聚集在一个搜索键上,这意味着我们在一个数据文件上最多可以有一个聚类索引。未聚类的索引称为未聚类索引;在一个数据文件上可以有几个未聚类的索引。假设student记录按年龄排序;按年龄排序存储数据条目的年龄索引是聚类索引。如果我们还在gpa字段上有一个索引,则后者必须是一个未聚类索引。

根据索引是否聚类,使用索引回答范围搜索查询的成本可能会有很大差异。如果索引是聚集的,则合格数据条目中的rid指向连续的记录集合,如图8.3所示,并且我们只需要检索几个数据页。如果索引未聚类,则每个符合条件的数据条目可能包含指向不同数据页的rid,从而导致与匹配范围选择的数据条目数量相同的数据页I/O !这一点将在第11章和第16章进一步讨论。

 8.4.2 密集索引与稀疏索引

如果索引为索引文件中记录中出现的每个搜索键值包含(至少)一个数据条目,则称索引是密集的。稀疏索引包含数据文件中每一页记录的一个条目。数据条目的Alternative(1)总是导致密集索引。Alternative(2)可用于构建密集索引或稀疏索引。Alternative(3)通常只用于构建密集索引。

我们在图8.5中展示了稀疏索引和密集索引。显示了一个包含三个字段(name、age和sal)的记录数据文件,上面有两个简单的索引,这两个索引都使用Alternative(2)作为数据输入格式。第一个索引是一个在名字上的稀疏的聚类索引。注意索引中数据项的顺序与数据文件中记录的顺序是如何对应的。每页数据记录有一个数据条目。第二个索引是age字段上的密集非聚类索引。注意,索引中数据条目的顺序与数据记录的顺序不同。数据文件中的每条记录在索引中有一个数据条目(因为我们使用了Alternative(2))。

 我们不能建立一个没有聚类的稀疏索引。因此,我们最多只能有一个稀疏索引。稀疏索引通常比密集索引小得多。另一方面,一些非常有用的优化技术依赖于索引的密集性(第16章)。

如果在一个字段上有一个密集的二级索引,则称该数据文件在该字段上是反向的。在一个完全倒置的文件中,每个字段上都有一个密集的二级索引,而这个二级索引没有出现在主键中。

 8.4.3 主索引和二级索引

 包含主键的一组字段上的索引称为主索引。不是主索引的索引称为二级索引。(术语primary index和secondary index有时有不同的含义:使用Alternative(1)的索引称为primary index,使用Alternative(2)或(3)的索引称为secondary index。我们将与前面提出的定义保持一致,但读者应该意识到文献中缺乏标准术语。)

如果两个数据项在与索引关联的搜索关键字字段中具有相同的值,则它们被称为重复项。主索引保证不包含重复项,但其他(字段集合)上的索引可以包含重复项。因此,一般来说,二级索引包含重复项。如果我们知道不存在重复项,也就是说,我们知道搜索键包含某个候选键,我们称该索引为唯一索引。

8.4.4 使用复合搜索键进行索引

索引的搜索键可以包含多个字段;这样的键称为复合搜索键或连接键。例如,考虑一个员工记录的集合,其中包含字段name、age和sal,并按名称排序存储。图8.6说明了键为<age,sal> 的复合索引,键为<sal,age>的复合索引,一个键为age的索引,一个键为sal的索引之间的区别。图中所示的所有索引都使用Alternative(2)作为数据条目。

 如果搜索键是复合的,则等式查询是将搜索键中的每个字段绑定到一个常量的查询。例如,我们可以请求检索age = 20且sal = 10的所有数据条目。散列文件组织只支持相等查询,因为只有在为搜索键中的每个字段指定值时,散列函数才能标识包含所需记录的桶。

在范围查询中,并非搜索键中的所有字段都绑定到常量。例如,我们可以请求检索age = 20的所有数据条目;该查询意味着sal字段可以接受任何值。作为范围查询的另一个示例,我们可以请求检索年龄< 30且sal > 40的所有数据条目。

9 树形结构索引

我们现在考虑基于树组织的两种索引数据结构,称为ISAM和B+树。这些结构为范围搜索提供了有效的支持,包括作为特殊情况的排序文件扫描。与排序文件不同,这些索引结构支持高效的插入和删除。它们还提供了对相等选择的支持,尽管在这种情况下它们不如基于哈希的索引有效,后者将在第10章中讨论。

ISAM 树是一种静态索引结构,在文件不经常更新时有效,但不适用于增长和收缩很多的文件。我们将在9.1节讨论ISAM。B+树是一种动态结构,可以优雅地适应文件中的变化。它是使用最广泛的索引结构,因为它能很好地适应变化,并支持相等查询和范围查询。我们将在9.2节介绍B+树。
我们将在其余部分详细介绍B+树。第9.3节描述了树节点的格式。第9.4节将讨论如何使用B+树索引来搜索记录。章节9.5给出了在B+树中插入记录的算法,章节9.6给出了删除记录的算法。第9.7节讨论如何处理重复项。最后,我们将在第9.8节讨论有关B+树的一些实际问题。

注释:根据第8章介绍的术语,在ISAM和B+树结构中,叶页包含数据条目。为方便起见,我们将搜索键值为k的数据条目表示为k *。非叶页包含形式为<search key value,page id>的索引项和用于指导搜索所需的数据条目(存储在某个叶子中)。我们通常只在上下文明确条目(索引或数据)的性质时使用条目。

9.1 索引顺序访问方法(isam)

为了理解ISAM技术的动机,从一个简单的排序文件开始是有用的。考虑一个按gpa排序的学生记录文件。要回答诸如“查找gpa高于3.0的所有学生”这样的范围选择,我们必须通过对文件进行二进制搜索来确定第一个这样的学生,然后从该点开始扫描文件。如果文件很大,初始的二进制搜索可能会非常昂贵;我们能改进这种方法吗?

一个想法是创建第二个文件,在原始(数据)文件中每页有一条记录,形式为<first key on page,pointer to page>,再次按键属性(在我们的示例中为gpa)排序。第二个索引文件中的页面格式如图9.1所示。

 我们用<key,pointer>作为条目。请注意,每个索引页包含的指针比键的数量多一个——每个键都用作分隔符,分隔由其左右指针指向的页的内容。这个结构如图9.2所示。

我们可以对索引文件进行二进制搜索,以识别包含满足范围选择的第一个键(gpa)值的页面(在我们的示例中,是gpa超过3.0的第一个学生),并沿着指针指向包含具有该键值的第一个数据记录的页面。然后,我们可以从该点开始依次扫描数据文件,以检索其他符合条件的记录。本例使用索引查找包含gpa大于3.0的Students记录的第一个数据页,并从该点开始扫描数据文件以检索其他此类Students记录。

因为索引文件中条目的大小(键值和页id)可能比页的大小小得多,并且数据文件的每页只存在一个这样的条目,所以索引文件可能比数据文件小得多;因此,索引文件的二进制搜索要比数据文件的二进制搜索快得多。但是,索引文件的二进制搜索仍然可能相当昂贵,而且索引文件通常仍然足够大,因此插入和删除的成本很高。

索引文件可能会很大,这激发了ISAM的想法:为什么不应用在索引文件上构建辅助文件的前一步,以此类推,直到最终的辅助文件适合一个页面?这种一级索引的重复构造导致了如图9.3所示的树状结构。ISAM索引的数据条目位于树的叶页和链接到某些叶页的附加溢出页中。此外,有些系统会仔细组织页面的布局,以便页面边界与底层存储设备的物理特性紧密对应。ISAM结构是完全静态的(除了溢出页之外,我们希望溢出页很少),并且有利于这种低级优化。

 每个树节点是一个磁盘页,所有数据驻留在叶页中。这对应于使用Alternative(1)作为数据条目的索引,根据第8章中描述的Alternative;我们可以用Alternative(2)创建索引,方法是将数据记录存储在一个单独的文件中,并存储<key, rid>在ISAM索引的页中对。在创建文件时,按顺序分配所有叶页,并根据搜索键值进行排序。(如果使用Alternatives(2)或(3),则在分配ISAM索引的叶页之前创建并排序数据记录。)然后分配非叶级页面。如果随后对文件进行了多次插入,那么将更多的条目插入到叶子中,而不是单个页面中,则需要额外的页面,因为索引结构是静态的。这些额外的页面是从溢出区域分配的。页面的分配如图9.4所示。

 插入、删除和搜索的基本操作都非常简单。对于相等选择搜索,我们从根节点开始,并通过比较给定记录的搜索字段中的值与节点中的键值来确定要搜索的子树。(搜索算法与B+树相同;稍后我们将详细介绍该算法。)对于范围查询,以类似的方式确定数据(或叶子)级别中的起始点,然后依次检索数据页。对于插入和删除,将确定用于搜索的适当页,并在必要时添加溢出页来插入或删除记录。

下面的示例说明了ISAM索引结构。考虑图9.5所示的树。所有的搜索都从根开始。例如,要定位键值为27的记录,我们从根开始并跟随左指针,因为27 < 40。然后我们跟随中间的指针,因为20 <= 27 < 33。对于范围搜索,我们找到第一个符合条件的数据条目作为相等选择,然后依次检索主叶页(还可以根据需要通过跟踪主页的指针检索溢出页)。假设主叶页是按顺序分配的——这个假设是合理的,因为这样的页的数量在创建树时是已知的,并且在随后的插入和删除操作中不会改变——因此不需要“下一个叶页”指针。

我们假设每个叶页可以包含两个条目。如果我们现在插入一条键值为23的记录,则条目23*属于第二个数据页,该数据页已经包含20*和27*,并且没有更多的空间。我们通过添加一个溢出页并在溢出页中放置23*来处理这种情况。溢出页链很容易形成。例如,插入48*、41*和42*会导致两个页面的溢出链。图9.5包含所有这些插入的树如图9.6所示。

项k *的删除是通过简单地删除该项来处理的。如果该条目位于溢出页上,并且溢出页变为空,则可以删除该页。如果条目位于主页面上,而删除使主页面为空,则最简单的方法是保留空的主页面;它用作未来插入的占位符(也可能是非空的溢出页,因为当在主页上删除创建空间时,我们不会将记录从溢出页移动到主页)。因此,主叶页的数量在文件创建时是固定的。注意,删除条目可能会导致出现在索引级别中的键值没有出现在叶子中!由于索引级别仅用于将搜索定向到正确的叶页,因此这种情况不是问题。删除条目42*、51*、97*后的图9.6树如图9.7所示。注意,删除51*后,键值51继续出现在索引级。对51*的后续搜索将转到正确的叶页,并确定条目不在树中。

非叶页引导搜索到正确的叶页。磁盘I/ o的数量等于树的层数,等于log FN,其中N是主叶页的数量,扇出F是每个索引页的子页数。这个数字远远小于二进制搜索的磁盘I/ o数,后者是log 2 N;实际上,通过将根页面固定在内存中可以进一步减少内存占用。通过一级索引访问的成本是log 2 (N/F)。如果我们考虑一个有1,000,000条记录的文件,每个叶子页有10条记录,每个索引页有100个条目,那么文件扫描的成本(以页I/ o计算)是100,000,对排序数据文件的二进制搜索是17,一级索引的二进制搜索是10,ISAM文件(假设没有溢出)是3。

注意,一旦创建了ISAM文件,插入和删除操作只会影响叶页的内容。这种设计的一个后果是,如果对同一叶片进行多次插入,就会形成很长的溢出链。这些链可以显著地影响检索记录的时间,因为当搜索到这个叶子时,还必须搜索溢出链。(尽管溢出链中的数据可以保持排序,但为了使插入更快,通常不会这样做。)为了缓解这个问题,最初创建树时,每个页面大约有20%是空闲的。但是,一旦用插入的记录填充了空闲空间,除非通过删除再次释放空间,否则只能通过完全重组文件来消除溢出链。

在并发访问方面,只有叶页被修改这一事实也具有重要的优势。当一个页面被访问时,它通常被请求者“锁定”,以确保该页的其他用户不会并发地修改它。要修改一个页,它必须被锁定在“独占”模式,只有当没有其他人持有该页上的锁时才允许。锁定会导致用户(更准确地说,是事务)排队等待访问页面。队列可能是一个重要的性能瓶颈,特别是对于索引结构根附近访问频繁的页面。在ISAM结构中,因为我们知道索引级页面永远不会被修改,所以我们可以安全地省略锁定步骤。不锁定索引级页面是ISAM相对于B+树等动态结构的一个重要优势。如果数据分布和大小是相对静态的,这意味着很少有溢出链,由于这个优势,ISAM可能比B+树更好。

9.2  B+树:动态索引结构

像ISAM索引这样的静态结构存在这样的问题:随着文件的增长,可能会产生较长的溢出链,从而导致较差的性能。这个问题促使开发更灵活、更动态的结构,以优雅地适应插入和删除。广泛使用的B+树搜索结构是一种平衡树,其中内部节点指导搜索,叶子节点包含数据条目。由于树结构是动态增长和收缩的,因此不能像在ISAM中那样按顺序分配叶页,在ISAM中,主叶页集是静态的。为了有效地检索所有叶页,我们必须使用页指针将它们链接起来。通过将它们组织成一个双链表,我们可以在任意一个方向上轻松地遍历叶页序列(有时称为序列集)。这个结构如图9.8所示。

 

以下是B+树的一些主要特征:

  • 对树的操作(插入、删除)使其保持平衡。
  • 如果实现第9.6节中讨论的删除算法,则保证除根节点外的每个节点的最小占用率为50%。但是,删除通常通过简单地定位数据条目并删除它来实现,而不需要根据需要调整树以保证50%的占用,因为文件通常会增长而不是收缩。
  • 搜索记录只需要从根遍历到适当的叶子。我们将把从根到叶子的路径长度——任何叶子,因为树是平衡的——作为树的高度。例如,一个只有叶子层和单个索引层的树,如图9.10所示,其高度为1。由于高扇形,B+树的高度很少超过3或4。

我们将研究B+树,其中每个节点包含m个条目,其中d≤m≤2d。值d是B+树的一个参数,称为树的阶数,是树节点容量的度量。根节点是对条目数量要求的唯一例外;对于根,只要求1≤m≤2d。

如果一个记录文件经常更新,并且排序访问很重要,那么维护一个B+树索引,将数据记录存储为数据条目,几乎总是优于维护一个排序文件。对于存储索引项的空间开销,我们获得了排序文件的所有优点以及高效的插入和删除算法。B+树通常保持67%的空间占用率。B+树通常也比ISAM索引更可取,因为插入被优雅地处理,没有溢出链。但是,如果数据集大小和分布保持相当静态,则溢出链可能不是主要问题。在这种情况下,有两个因素有利于ISAM:叶页按顺序分配(使得在大范围内的扫描比在B+树中更有效,在B+树中,随着时间的推移,页可能会在磁盘上打乱顺序,即使它们在批量加载后是按顺序排列的),ISAM的锁定开销低于B+树。然而,作为一般规则,B+树可能比ISAM表现得更好。

9.3 节点的格式

节点格式与ISAM相同,如图9.1所示。具有m个索引项的非叶节点包含m + 1个指向子节点的指针。指针pi指向一个子树,其中所有键值K都满足K i≤K < K i+1。作为特殊情况,p0指向所有键值小于k1的树,pm指向所有键值大于或等于K m的树。对于叶节点,条目通常记为k *。就像在ISAM中一样,叶节点(而且只有叶节点!)包含数据条目。在通常情况下使用Alternative(2)或(3),叶条目是<K,I(K) >对,就像非叶元素一样。不管为叶条目选择了什么替代方法,叶页面都在一个双重链表中链接在一起。因此,叶子形成一个序列,可用于有效地回答范围查询。

读者应该仔细考虑如何使用7.7节中提供的记录格式来实现这样的节点组织;毕竟,每个键-指针对都可以看作是一条记录。如果被索引的字段是固定长度的,则这些索引条目将是固定长度的;否则,我们有变长记录。在任何一种情况下,B+树本身都可以被视为一个记录文件。如果叶页不包含实际的数据记录,那么B+树确实是一个记录文件,它不同于包含数据的文件。如果叶页包含数据记录,则文件包含B+树和数据。

9.4 搜索

搜索算法查找给定数据条目所属的叶节点。该算法的伪代码草图如图9.9所示。使用*ptr表示法表示指针变量ptr所指向的值,使用& (value)表示法表示value的地址。注意,在树搜索中找到 i 需要我们在节点内搜索,这可以通过线性搜索或二叉搜索来完成(例如,取决于节点中的条目数量)。

在讨论B+树的搜索、插入和删除算法时,我们将假设没有重复项。也就是说,不允许两个数据项具有相同的键值。当然,只要搜索键不包含候选键,就会出现重复,必须在实践中处理。我们将在9.7节讨论如何处理重复项。

考虑图9.10中所示的样例B+树。这个B+树是d=2阶的。即每个节点包含2到4个条目。每个非叶条目都是一对<key value, nodepointer>;在叶级,条目是我们用k *表示的数据记录。为了搜索条目5*,我们跟踪最左边的子指针,因为5 < 13。为了搜索14*或15*,我们跟踪第二个指针,因为13≤14 < 17,并且13≤15 < 17。(我们没有在相应的叶子上找到15*,我们可以得出结论,它不存在于树中。)为了找到24*,我们跟踪第四个子指针,因为24≤24 < 30。

 

 9.5 插入

插入算法取一个条目,找到它所属的叶节点,然后将它插入到那里。图9.11给出了B+树插入算法的伪代码。该算法背后的基本思想是,我们通过在适当的子节点上调用插入算法来递归地插入条目。通常,这个过程会导致向下到条目所属的叶节点,将条目放置在那里,然后一路返回到根节点。有时节点已满,必须拆分它。当节点被拆分时,一个指向拆分创建的节点的条目必须插入到它的父节点中;该条目由指针变量newchildtry指向。如果(旧的)根被分割,则创建一个新的根节点,并且树的高度增加1。

为了演示插入,让我们继续使用图9.10所示的示例树。如果我们插入条目8*,它属于最左边的叶子,它已经满了。这个插入会导致页的分裂;拆分页面如图9.12所示。现在必须调整树以考虑到新的叶页,因此我们插入一个包含pair 5的条目,指向新页的指针?到父节点。注意键5是如何“向上复制”的,它区分了拆分叶页面和新创建的同级页面。我们不能只是“向上推”5,因为每个数据条目都必须出现在一个页中。

由于父节点也是满的,因此会发生另一次分割。一般来说,我们必须在非叶节点满时拆分它,它包含2d键和2d + 1指针,并且我们必须添加另一个索引条目来考虑子拆分。我们现在有2d+ 1个键和2d+2个指针,产生了两个最小满的非叶节点,每个节点包含d个键和d+1个指针,以及一个额外的键,我们选择它作为“中间”键。这个键和指向第二个非叶节点的指针构成一个索引项,必须插入到拆分的非叶节点的父节点中。因此,中间键被“推到”树的上方,这与叶页的分裂情况形成了对比。

在我们的例子中拆分页面如图9.13所示。指向新的非叶节点的索引条目是pair <17, pointer to new index-level page>;注意,键值17在树中被“向上推”,而在叶子分裂中,键值5被“向上复制”。

处理叶级和索引级拆分的不同之处在于B+树要求所有数据项k *必须驻留在叶级。这个要求阻止了我们“上推”5,并导致一些键值出现在叶级和一些索引级的轻微冗余。但是,仅通过检索叶页的序列就可以有效地回答范围查询;与提高效率相比,裁员只是一个小小的代价。在处理索引级别时,我们有更大的灵活性,我们“上推”17以避免在索引级别中有两个17的副本。

 

 现在,由于分割节点是旧的根节点,我们需要创建一个新的根节点来保存区分两个分割索引页的条目。完成8*项插入后的树如图9.14所示。

插入算法的一种变体尝试在拆分节点之前,用兄弟节点重新分配节点N的条目;这提高了平均入住率。在这种情况下,节点N的兄弟节点是紧挨着N的左边或右边并且与N具有相同父节点的节点。
为了说明重新分配,请重新考虑将8*项插入到图9.10所示的树中。条目属于最左边的叶子,它是满的。然而,这个叶节点的(唯一的)兄弟节点只包含两个条目,因此可以容纳更多的条目。因此,我们可以通过重新分配来处理8*的插入。注意,父节点中指向第二个叶节点的条目有一个新的键值;我们将新的低键值“复制”到第二个叶子上。这个过程如图9.15所示。

为了确定重新分配是否可能,我们必须检索同级。如果同级节点恰好已满,我们无论如何都必须拆分节点。平均而言,检查重新分配是否可能会增加索引节点拆分的I/O,特别是在检查两个兄弟节点时。(检查重新分配是否可能,如果重新分配成功,而拆分在树中传播,可能会减少I/O,但这种情况很少发生。)如果文件在增长,即使我们不重新分配,平均占用率可能也不会受到太大影响。考虑到这些因素,在非叶级不重新分配条目通常是有益的。

但是,如果在叶节点级别发生分裂,我们必须检索一个邻居,以便根据新创建的叶节点调整前一个和下一个邻居指针。因此,一种有限形式的再分配是有意义的:如果一个叶节点已满,则获取一个邻居节点;如果它有空间,并且有相同的父节点,则重新分配条目。否则(邻居有不同的父节点,即不是兄弟节点,或者也是满节点)拆分叶节点并调整拆分节点中的前一个和下一个邻居指针、新创建的邻居和旧邻居。

 9.6 删除

删除算法获取一个条目,找到它所属的叶节点,然后删除它。B+树删除算法的伪代码如图9.16所示。该算法背后的基本思想是,通过在适当的子节点上调用delete算法来递归地删除条目。我们通常会到条目所在的叶节点,从那里删除条目,然后再返回到根节点。有时,一个节点在删除之前处于最小占用状态,删除会导致它低于占用阈值。当这种情况发生时,我们必须从相邻的兄弟节点重新分配条目,或者将节点与兄弟节点合并,以保持最小的占用率。如果条目在两个节点之间被重新分配,它们的父节点必须更新以反映这一点;指向第二个节点的索引条目中的键值必须更改为第二个节点中最低的搜索键值。如果两个节点合并,则必须通过删除第二个节点的索引条目来更新它们的父节点以反映这一点;当delete调用返回到父节点时,指针变量oldchildtry指向这个索引项。如果根节点中的最后一个条目以这种方式被删除,因为它的一个子节点被删除了,那么树的高度将减少1。

为了说明删除,让我们考虑图9.14所示的示例树。要删除条目19*,我们只需将它从它所在的叶页面中删除,这样就完成了,因为叶页面仍然包含两个条目。但是,如果随后删除20*,则在删除后叶子只包含一个条目。包含20*的叶节点(唯一的)兄弟节点有三个条目,因此我们可以通过重新分配来处理这种情况;我们将条目24*移动到包含20*的叶页,并将新的分割键(27,这是我们从中借用24*的叶页的新的低键值)“复制”到父页。这个过程如图9.17所示。

假设我们现在删除条目24*。受影响的叶子在删除后只包含一个条目(22*),而(唯一的)兄弟只包含两个条目(27*和29*)。因此,我们不能重新分配条目。但是,这两个叶节点加在一起只包含三个条目,并且可以合并。在合并时,我们可以“抛出”条目(<27、pointer to second leaf page>)在父节点中,其中指向的是第二叶页,因为合并后的第二叶页是空的,可以丢弃。删除条目24*后,图9.17的右子树如图9.18所示。

删除<pointer to second leaf page>创建了一个只有一个条目的非叶级页面,该页面低于d=2的最小值。要解决这个问题,我们必须重新分配或合并。无论哪种情况,我们都必须取一个兄弟。该节点的唯一兄弟节点只包含两个条目(键值为5和13),因此不可能重新分配;因此,我们必须合并。

 

 合并两个非叶节点的情况和拆分一个非叶节点的情况是完全相反的。当一个非叶节点包含2d键和2d + 1指针时,我们必须拆分它,我们必须添加另一个键-指针对。因为只有当我们不能在两个非叶节点之间重新分配条目时,我们才会合并它们,所以两个节点必须是最小满的;也就是说,每个对象在删除之前必须包含d个键和d+1个指针。合并两个节点后,移除要删除的键-指针对,得到2d−1键和2d+1指针:直观上,第二个合并节点上最左边的指针缺少键值。要查看必须将哪个键值与该指针组合以创建完整的索引项,请考虑合并的两个节点的父节点。指向合并节点之一的索引条目必须从父节点中删除,因为该节点即将被丢弃。这个索引项中的键值正是我们完成新合并节点所需的键值:第一个被合并节点中的项,然后是从父节点“拉下”的拆分键值,然后是第二个非叶节点中的项,我们总共得到了2d个键和2d + 1个指针,这是一个完整的非叶节点。注意父节点中的拆分键值是如何“下拉”的,这与合并两个叶节点的情况不同。

在我们的示例中考虑两个非叶节点的合并。非叶节点和要合并的兄弟节点加在一起只包含三个条目,它们总共有五个指向叶节点的指针。为了合并这两个节点,我们还需要“下拉”它们的父节点中当前区分这两个节点的索引条目。这个索引项的键值为17,因此我们创建了一个新条目<17, left-most child pointer in sibling>中最左边的子指针。现在我们总共有四个条目和五个子指针,它们可以放在d=2阶树的一个页面上。注意,拉下拆分键17意味着它将不再出现在合并后的父节点中。在我们通过将所有条目放在一个页面上并丢弃空的兄弟页面来合并受影响的非叶节点及其兄弟节点之后,新节点是旧根的唯一子节点,因此可以丢弃它。完成所有步骤删除条目24*后的树如图9.19所示。

 前面的示例说明了跨叶的条目重新分配以及叶级和非叶级页面的合并。另一种情况是在非叶级页面之间重新分配条目。为了理解这种情况,考虑图9.18所示的中间右子树。如果我们尝试从类似于图9.17所示的树中删除24*,但使用图9.20所示的左子树和根键值,我们将到达相同的中间右子树。图9.20中的树显示了删除24*的中间阶段。(尝试构造初始树。)

与我们从图9.17的树中删除24*的情况相反,非叶包含键值为30的关卡节点现在有一个兄弟节点,可以节省条目(条目)键值为17和20)。我们把这些元素从兄弟元素中移开。请注意,在这样做的过程中,我们实际上是将它们“推”过父节点中的拆分条目(根),它处理了这样一个事实,即17成为
正确,因此必须替换根中的旧分割键(键值为22)。所有这些变化的树如图9.21所示。

在结束对删除的讨论时,我们注意到我们只检索的一个兄弟一个节点。如果这个节点有多余的条目,我们使用重新分配;否则,我们归并。如果节点有第二个兄弟节点,那么也应该检索这个兄弟节点,以检查重新分配的可能性。重新分配很有可能实现,并且与合并不同,重新分配保证不会传播到父节点以外的地方。此外,页面上有更多的空间,这减少了后续插入时分裂的可能性。(请记住,文件通常会增长,而不是缩小!)然而,这种情况出现的次数(节点少于半满并且第一个兄弟节点不能腾出一个条目)不是很高,因此没有必要实现我们所提出的基本算法的这种改进。

 9.7 副本

我们介绍的搜索、插入和删除算法忽略了重复键的问题,即具有相同键值的多个数据条目。现在我们讨论如何处理重复。

基本搜索算法假定具有给定键值的所有条目驻留在单个叶页面上。满足这一假设的一种方法是使用溢出页来处理重复项。(当然,在ISAM中,我们在任何情况下都有溢出页,并且很容易处理重复页。)

但是,通常我们对副本使用另一种方法。我们像处理任何其他条目一样处理它们,并且几个叶页可能包含具有给定键值的条目。要检索具有给定键值的所有数据条目,我们必须搜索具有给定键值的最左边的数据条目,然后可能检索多个叶页(使用叶序列指针)。修改搜索算法以查找具有重复项的索引中最左边的数据项是一个有趣的练习(实际上,它是练习9.11)。

这种方法的一个问题是,当删除一条记录时,如果我们对数据条目使用Alternative(2),那么在B+树索引中查找要删除的相应数据条目可能效率很低,因为我们可能需要检查几个重复的条目:key, rid?使用相同的键值。这个问题可以通过考虑数据条目中的rid值作为搜索键的一部分来解决,以便在树中定位数据条目。该解决方案有效地将索引转换为唯一索引(即没有重复索引)。请记住,搜索键可以是任何字段序列—在这种变体中,在构造搜索键时,数据记录的删除实际上被视为另一个字段。

商业系统中的重复处理:在Sybase ASE中的聚集索引中,在页面上和数据页面集合中按排序顺序维护数据行。数据页按排序顺序双向链接。将具有重复键的行插入(或从中删除)有序的行集。这可能导致将具有重复键的行的溢出页插入到页链中,或者从页链中删除空溢出页。插入或删除重复键不会影响较高的索引级别,除非发生非溢出页的分割或合并。在IBM DB2、Oracle 8和Microsoft SQL Server中,如果有必要,可以通过添加行id来消除重复的键值来处理重复。

数据条目的Alternative(3)为重复项提供了一种自然的解决方案,但是如果我们有大量重复项,单个数据条目可能跨越多个页面。当然,当删除数据记录时,从相应的数据条目中查找要删除的rid可能效率很低。这个问题的解决方案类似于上面针对Alternative(2)讨论的解决方案:我们可以在每个数据条目中按排序顺序维护rid列表(例如,如果一个rid包含一个页id和一个槽id,则按页码,然后是槽号)。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值