索引可以是“稠密的”,即数据文件中每个记录在索引文件中都设有一个索引项;索引也可以是“稀疏的”,即数据文件中只有一些记录在索引文件中表示出来,通常为每个数据块在索引文件中设一个索引项。索引还可以是“主索引”或者“辅助索引”。主索引能确定记录在数据文件中的位置,而辅助索引不能。比如说,通常我们会在关系的主键上建立主索引,而在其他的属性上建立辅助索引。
3.1.1 顺序文件
是对关系中的元组按主键进行排序而生成的文件。关系中的元组按照这个次序分布在多个数据块中。
3.1.2 稠密索引
如果记录是排好序的,我们就可以在记录上建立稠密索引,它是这样一系列存储块:块中只存放记录的键以及指向记录本身的指针,指针就是如2.6节讨论的那样的地址。稠密索引文件中的索引块保持键的顺序与文件中的排序顺序一致。既然我们假定查找键和指针所占存储空间远小于记录本身,我们就可以认为存储索引文件比存储数据文件所需存储块要少得多。当内存容纳不下数据文件,但能容纳下索引文件时,索引的优势尤为明显。这时,通过使用索引文件,我们每次查询只用一次I/O操作就能找到给定键值的记录。
例3.2图3-2所示为一个建立在顺序文件上的稠密索引。
图3-2 顺序文件(右)上的稠密索引(左) |
第一个索引块存放指向前四个记录的指针,第二个索引块存放指向接下来的四个记录的指针,依此类推。
稠密索引支持按给定键值查找相应记录的查询。给定一个键值K,我们先在索引块中查找K。当找到K后,我们按照K所对应的指针到数据文件中找到相应的记录。似乎在找到K之前我们需要检索索引文件的每个存储块,或平均一半的存储块。然而,由于有下面几个因素,基于索引的查找比它看起来更为有效:
1.索引块数量通常比数据块数量少。
2.由于键被排序,我们可以使用二分查找法来查找K。若有n个索引块,我们只需查找log2n个块。
3.索引文件可能足够小,以至可以永久地存放在主存缓冲区中。要是这样的话,查找键K时就只涉及主存访问而不需执行I/O操作。
3.1.3 稀疏索引
稀疏索引只为数据文件的每个存储块设一个键-指针对,它比稠密索引节省了更多的存储空间,但查找给定值的记录需更多的时间。只有当数据文件是按照某个查找键排序时,在该查找键上建立的稀疏索引才能被使用,而稠密索引则可以应用在任何的查找键。如图3-3所示,稀疏索引只为每个存储块设一个键-指针对。键值是每个数据块中第一个记录的对应值。
例3.3同例3.2一样,我们假定数据文件已排序,且其键值为连续的10的倍数,直至某个较大的数。我们还继续假定每个存储块可存放四个键-指针对。这样,第一个索引存储块中为前四个数据存储块的第一个键值的索引项,它们分别是10、30、50和70。按照前面假定的键值模式,第二个索引存储块中为第五至第八个数据存储块的第一个键值的索引项,它们分别是90、110、130和150。图中我们还列出第三个索引存储块存放的键值,它们分别是假设的第九至第十二个数据存储块的第一个键值。
图3-3 顺序文件上的稀疏索引 |
在已有稀疏索引的情况下,要找出查找键值为K的记录,我们得在索引中查找到键值小于或等于K的最大键值。由于索引文件已按键排序,我们可以使用二分查找法来定位这个索引项,然后根据它的指针找到相应的数据块。现在我们必须搜索这个数据块以找到键值为K的记录。当然,数据块中必须有足够的格式化信息来标明其中的记录及记录内容,可以采用2.5节和2.7节中的任何技术。
3.1.4 多级索引
索引文件可能占据多个存储块,即便我们能定位索引存储块,并且能使用二分查找法找到所需索引项,我们仍可能需要执行多次I/O操作才能得到我们所需的记录。通过在索引上再建索引,我们能够使第一级索引的使用更为有效。
图3-4对图3-3进行了扩展,它是在图3-3的基础上增加二级索引层(和前面一样,我们假设使用10的连续倍数这一不常见的模式)。按照同样想法,我们可以在二级索引的基础上建立三级索引,等等。然而,这种做法有它的局限,与其建立多级索引,我们宁愿考虑使用在3.2节讲述的B-树。
图3-4增加一个二级稀疏索引 |
在这个例子中,一级索引是稀疏的,虽然我们也可以选择稠密索引来作为一级索引。但是,二级和更高级的索引必须是稀疏的,原因在于一个索引上的稠密索引将需要和其前一级索引同样多的键-指针对,因而也就需要同样的存储空间。
3.1.5 辅助索引
辅助索引可用于任何索引目的:这种数据结构有助于查找给定一个或多个字段值的记录。但是,辅助索引与主索引最大的差别在于辅助索引不决定数据文件中记录的存放位置。而仅能告诉我们记录的当前存放位置,这一位置可能是由建立在其他某个字段上的主索引确定的。辅助索引和主索引这一差别有一个有趣的推论:
辅助索引总是稠密索引。谈论一个稀疏的辅助索引是毫无意义的。因为辅助索引不影响记录的存储位置,我们也就不能根据它来预测键值不在索引中显式指明的任何记录的位置。
例3.4图3-5所示为一个典型的辅助索引。与我们前面图示的准则一样:数据文件中每个块存放二个记录。记录只显示了各自的查找键;其值为整型,而且像前面一样我们给它们取值为10的倍数。要注意,与图3-2中的数据文件不同,这里的数据没有按查找键排序。
图3-5 辅助索引 |
然而,索引文件中的键是排序的。这样就造成索引块中的指针并不是指向一个或少数几个连续存储块,而是指向许多不同的数据块。例如,为了检索键值为20的所有记录,我们不仅要查找两个索引块,而且还得访问指针指向的三个不同的数据块。因此,查找同样数量的记录,使用辅助索引比使用主索引可能需要多得多的磁盘I/O。但是这个问题是无法解决的,我们无法控制数据块中的元组顺序,因为这些元组可能已按其他属性排序。
3.1.6 辅助索引的运用
除了能在被组织成顺序文件的关系上建立附加索引外,辅助索引甚至还用作某些数据结构的主键索引。这些结构之一就是“堆”(heap)结构,在这种结构中,关系的记录之间没有特定的顺序。
第二种需要辅助索引的常见数据结构是聚集文件。假设有关系R和S,R中的元组和S中的元组具有多对一的对应关系。一种组织结构是把关系R的每个元组和关系S中相关的元组存储在一起,另一种结构是按照主键来存储关系R。前一种结构在某些情况下更加合理。下面的一个例子说明了这种组织结构在特定情况下的合理性。
例3.5考虑movie和studio两个标准的关系:
进一步假定查询的常见形式如下:
这里,zzz可以表示任意制片厂经理的证件号,也就是说,已知一个制片厂的经理,我们需要找到由该制片厂制作的所有电影。
要是我们确信上面这类查询是典型的查询,那么我们就可不按主键title和year排序,而是为Studio和Movie两个关系建立一个聚集文件结构,如图3-6所示。我们在每个Studio的元组后面存放关系Movie中该制片厂的所有电影元组。
图3-6 将制片厂及其制作的影片聚集在一起的聚集文件 |
如果我们为关系Studio在查找键presC#上建立索引,那么不管zzz是什么,我们都可以快速地找到所有符合条件的制片厂的元组。并且,Movie中所有studioName属性和某个制片厂的name属性匹配的元组,都会在聚集文件中紧跟在该制片厂的元组后出现。这样的话,我们可以用尽量少的几次I/O就找到该制片厂的所有电影,因为要查找的Movie元组已经以尽可能稠密的方式存储在紧跟着的数据块里。尽管如此,在Movie上对任意属性建立的索引只能是辅助索引。
3.1.7 辅助索引中的间接
图3-5所示结构存在空间浪费,有时浪费很大。假如某个索引键值在数据文件中出现n次,那么这个键值在索引文件中就要写n次,如果我们只为指向该键值的所有指针存储一次键值,这样会比较好。
避免键值重复的一种简便方法是使用一个称为桶的间接层,它介于辅助索引文件和数据文件之间。如图3-7所示,每个查找键K有一个键-指针对,指针指向一个桶文件,该文件中存放K的桶。从这个位置开始,直到索引指向的下一个位置,其间指针指向索引键值为K的所有记录。
图3-7 通过在辅助索引中使用间接层以节省空间 |
例3.6例如在图3-7的索引文件中,我们沿索引键为50的索引项指针找到中间“桶”文件。这一指针刚好将我们带到桶文件中第一个块的最后一个指针。我们继续向前查找,找到下一块的第一个指针。因为索引文件中键值为60的索引项指针刚好指向桶文件的第二个块的第二个指针,所以我们停止查找。
在图3-7所示的方式中,只要查找键值的存储空间比指针大并且每个键平均出现至少两次,就可以节省空间。不过,即使在键值和指针大小相当的情况下,在辅助索引上使用间接层也有一个重要的好处:我们通常可以在不访问数据文件记录的前提下利用桶的指针来帮助回答一些查询。特别是,当查询有多个条件,而每个条件都有一个可用的辅助索引时,我们可以通过在主存中将指针集合求交来找到满足所有条件的指针,然后只需要检索交集中指针指向的记录。这样,我们就节省了检索满足部分条件而非所有条件的记录所需的I/O开销
假若我们直接从索引而非桶中取得指针,也可以使用这一指针求交技巧。 。
例3.7考虑常用的Movie关系。
假定我们在studioName和year上都建立了有间接桶的辅助索引,而且我们要执行如下查询:
即找出Disney在2005年制作的所有电影。
图3-8说明我们如何使用索引来回答这个查询。通过studioName上的索引,我们找出了所有指向Disney制作的电影的指针。但是,我们并不把这些记录从磁盘上取到主存中,而是通过year上的索引,再找出所有指向2005年制作的电影的指针。然后我们求两个指针集的交集,正好得到2005年Disney制作的所有电影。最后我们到磁盘上去检索所有包含一部或几部这样的电影的块,这样只需检索尽可能少的数据块。
图3-8 在主存中求桶的交集 |
3.1.8 文档检索和倒排索引
多年来,信息检索界都在研究文档的存储和按关键字集高效检索文档的问题。随着WWW出现以及在线保存所有文档成为可能,基于关键字的文档检索已成为数据库最大的难题之一。尽管可用来找出相关的文档的查询有多种,但最简单、最常见的形式可用关系的术语描述为:
一个文档可被看成是关系Doc的元组。这个关系有很多的属性,每个属性对应于文档可能出现的一个词。每个属性都是布尔型的—表明该词在该该文档出现还是没有出现。因此,这一关系模式可以被看作:
其中hasCat取值为真当且仅当该文档中至少出现一次“cat”这个词。
关系Doc的每个属性上都建有辅助索引。不过,我们不必费心为属性值为FALSE的元组建索引项;相反,索引只会将我们带到出现该词的那些文档。也就是说,索引中只有查找键值为TRUE的索引项。
我们不是给每个属性(即每个词)建立一个单独的索引,而是把所有的索引合成一个,称为倒排索引。这个索引使用间接桶来提高空间利用率,正如3.1.7节中讨论的那样。
例3.8图3-9所示为一个倒排索引。取代记录数据文件的是一个文档集合,每个文档可以被存放在一个或多个磁盘块上。倒排索引本身由一系列词-指针对组成;词实际上是索引的查找键。正如目前为止讨论的任何一种索引那样,倒排索引被存储在连续的块中。
图3-9 文档的倒排索引 |
指针指向“桶”文件中的位置。例如,在图3-9中,“cat”一词有一个指针指向桶文件。该指针指向所有包含“cat”的文档的指针列表的表头。图中给出了一些这样的指针。类似地,图中“dog”一词的指针指向一个指针列表,该列表中指针指向包含“dog”的所有文档。
桶文件中指针可以是:
1.指向文档本身的指针。
2.指向词的一个出现的指针。在这种情况下,指针可以是由文档的第一个块和一个表示该词在文档中出现次数的整数构成的对。
当我们使用指针“桶”指向每个词的多次出现的时候,我们可能就会想扩展这个想法,使桶数组包含更多有关词的出现的信息。这样,桶文件本身就成了有重要结构的记录集合。这种做法早期应用在区分一个词出现在文档的题目、摘要还是正文中的情况。随着Web上文档的增长,尤其是使用HTML、XML或其他标记语言的文档的增长,我们也可以指明与词关联的标记。例如,我们不仅可以区分出现在题头、表或锚中的词,而且可以区分以不同字体和字号出现的词。
例3.9图3-10所示为一个标明HTML文档中词的出现情况的桶文件。如果有出现类型(即标记),就在第一列指明。第二、第三列一起构成指针指向词的出现:第三列指明文档,而第二列给出了该文档中该词出现的位置。
桶中的插入与删除
在一些图例如图3-9中,我们所给的桶是大小适中的紧凑数组。实际上,它们是单个字段(指针)的记录,且像其他任何记录集合一样存放在块中。因此在插入和删除指针时,我们可用目前为止学过的任一种技术,例如为文件的扩充预留空闲空间、溢出块和可能的块内或块间记录移动。在后一种情况下,当我们移动倒排索引和桶中指针指向的记录时,我们必须小心地改变从倒排索引到桶文件中的相应指针。
我们可以用这种数据结构来回答关于文档的各种查询,而且不用仔细查看文档。例如,假设我们想找出有关狗的并将狗与猫作了比较的文档;没有深刻理解文档的内容,我们就无法准确地回答这个查询。但是,要是我们查找符合以下条件的文档,我们可以获得一个很好的提示:
a)在标题中提到dog(狗)。
b)在某个锚中提到cat(猫)——该锚可能是连到一个关于猫的文档的链接。
图3-10 在倒排索引中存储更多的信息 |
对信息检索的进一步讨论
有很多技术可用于改进基于关键字的文档检索效率。尽管完整介绍这些技术已超出本书的范围,这里介绍两种有用的技术:
1.抽取词干。在将单词的出现放入索引中之前,我们删除词的后缀以找出它的“词干”。例如,复数名词可被当作其单数形式处理。因此,例3.8中的倒排索引显然使用了抽取词干技术,因为搜索“dog”一词时我们不仅得到“dog”的文档,而且得到一个有“dogs”的文档。
2.无用词。最常用像“the”、“and”之类的词称为无用词,通常不包含在倒排索引中。原因在于好几百个常用词出现在太多的文档中,以至于它根本无益于检索特定主题。去除无用词还可以明显地缩小索引。
我们可以通过对指针求交来回答这个查询。也就是说,我们按对应于“cat”的指针找到这一单词的所有出现。我们从桶文件中选择有“cat”出现且类型为“锚”的文档指针。接着,我们找到“dog”的桶中项目,并从中选择类型为“标题”的文档指针。如果我们把这两个指针集相交,就得到符合在标题中提到“dog”且在某个锚中提到“cat”这一条件的文档。