目录
第十一章 索引与散列
11.1 基本概念
数据库系统中的文件索引的作用是加速查询。
两种基本类型的索引:
- 顺序索引:基于值的顺序排序
- 散列索引:基于将值平均分道若干散列桶中。一个值所属的散列桶是由散列函数(hash function)决定的。
多索引的评价包括:
- 访问类型(access type):能有效支持的访问类型,可以包括:找到具有特定属性值的记录,以及找到属性值落在某个特定范围内的记录。
- 访问时间(access time):在查询中使用该技术找到一个特定数据项或数据项集所需的时间。
- 插入时间(insertion time):插入一个新数据项的时间。包括:插入新数据项到正确位置所需的时间+更新索引结构的时间。
- 删除时间(deletion time):删除一个数据项所需的时间。包括:找到待删除项所需的时间+更新索引结构的时间。
- 空间开销(space overhead):索引结构所占用的额外的存储空间。倘若索引结构占用的空间适合,通常可以牺牲一定的空间的额代价换时间。
搜索码(search key):用于在文件中查找记录的属性或属性集。一个文件可以有多个搜索码。
11.2 顺序索引
一个文件可以有多个索引,分别基于不同的搜索码。
聚集索引(clustering index)/主索引(primary index):如果包含记录的文件是按照某个搜索码指定的顺序排序,那么该搜索码对应的索引称为聚集索引/主索引。
非聚集索引(nonclustering index)/辅助索引(secondary index):搜索码定义的顺序与文件中记录的物理顺序不同的索引,称为非聚集索引/辅助索引。
在搜索码上有聚集索引的文件 称作 索引顺序文件(index-sequential file)。
顺序索引的缺点:随着文件的增大,索引查找西能和数据顺序扫描性能都会下降。
11.2.1 稠密索引和稀疏索引
索引项(index entry)或索引记录(index record): 由 一个搜索码值 + 指向具有该搜索码值的一条或多条记录的指针 构成。
指向记录的指针,包括:磁盘块的标识+表示磁盘块内记录的块内偏移量。
顺序索引分为两类:
- 稠密索引(dense index):稠密索引中,文件中的每个搜索码值都有一个索引项。
- 稠密聚集索引:索引项包括搜索码值 + 指向具有该搜索码值的第一条记录的指针,具有相同搜索码值的记录 顺序地存储在第一条数据记录之后。记录根据搜索码值进行排序。
- 稠密非聚集索引:索引项必须存储指向所有具有相同搜索码值的记录。
- 稀疏索引(sparse index):稀疏索引中,只为搜索码的某些值建立索引项。只有当关系按照搜索码顺序存储时才能使用稀疏索引,换句话说,只有聚集索引才能使用稀疏索引。索引项由 搜索码值+指向具有该搜索值的第一条记录的指针。
- 为了定位一条记录,先找到最大搜索码值小于所查找记录的搜索码值的索引指针,然后从该指针指向的记录开始顺序查找,直到找到记录为止。
稠密索引 VS 非稠密索引
稠密索引定位记录更快,但是非稠密索引占据的空间较小,并且插入和删除时维护索引的开销也较小。
具体使用时,必须在时间和空间上进行权衡。但是为每个块建立稀疏索引也是一个较好的折中。这是因为:
- 处理数据库查询的开销主要在于把块读入到主存的时间决定。
- 一旦记录放入主存,则扫描整个块的时间可以忽略。
- 使用这样的稀疏索引,可以定位包含索要查找记录的块,这样只要记录不在溢出块,就能使访问次数最小,同时保持索引尽可能小。
11.2.2 多级索引
如果索引项小到可以放到主存中,搜索一个索引项的时间就会很短。
如果索引项过大而不能放到主存中,那么当需要时就必须从磁盘中读出索引块,于是会导致搜索一个索引项需要多次读取磁盘块。
顺序稠密索引 VS 多级顺序索引
- 顺序稠密索引:
- 可以利用二分法来定位索引项,复杂度为log2(b),其中b为索引占据的磁盘块的数量。
- 例如:占用10,000个块的索引,需要14次读块操作。假设读取一个块10毫秒,那么搜索将耗时140毫秒。
- 注意:如果使用了溢出块,则不能使用二分搜索。
- 可以利用二分法来定位索引项,复杂度为log2(b),其中b为索引占据的磁盘块的数量。
- 多级(multilevel)顺序索引:
- 二级索引:除了构建稠密索引作为内层索引外,还在外层构建了稀疏索引。通常外层索引能够存储在主存中
- 定位一条记录,首先二分法查找外层索引,找到其最大搜索码值小于或等于所需搜索码值的记录,指针指向一个内层索引块。直到找到最大搜索码小于或者等于所查记录的文件块。
- 例如,占用10,000个块的索引,内层索引10,000个。假设外层索引已经存储在主存中,那么使用多级索引时,一次查询只需读取1个块,远小于顺序稠密索引的14个块。
- 定位一条记录,首先二分法查找外层索引,找到其最大搜索码值小于或等于所需搜索码值的记录,指针指向一个内层索引块。直到找到最大搜索码小于或者等于所查记录的文件块。
- 多级索引:若外层索引无法存储在主存中,可以创建多级索引。
- 二级索引:除了构建稠密索引作为内层索引外,还在外层构建了稀疏索引。通常外层索引能够存储在主存中
11.2.3 索引的更新
每当文件中有记录插入或者删除时,索引都需要更新。此外,如果文件中的记录更新,任何搜索码属性受影响的索引也必须更新。
记录的更新可以设计为:先删除,再插入。
11.2.4 辅助索引
辅助索引必须是稠密索引!!!
聚集索引可以是稀疏索引,也可以是稠密索引。
辅助索引能够提高使用聚集索引搜索码以外的码的查询性能;但是也显著增加了数据库更新的开销。
可以用一个附加的间接指针层来实现非候选码的搜索码上的辅助索引。在这样的辅助索引中,指针不直接指向文件,而是指向一个包含文件指针的桶。
11.2.5 多码上的索引
复合搜索码(composite search key):一个包含多个属性的搜索码称为复合搜索码。
这种索引的结构和其他索引类似,唯一不同的是搜索码是属性列表。
11.3 B+树索引文件
索引顺序文件的缺点:随着文件的增大,索引查询性能和数据顺序扫描性能都会下降。虽然这种性能下降可以通过对文件进行重新组织来弥补,但是我们不希望频繁的进行重组。
B+树索引结构:采用平衡树(balance tree)结构,其中树根到树叶的每条路径的长度相同,树中每个非叶节点有[n/2]~n个children,其中n代表是n叉平衡树。
11.3.1 B+树的结构
B+树索引具有良好的查找、插入和修改功能得益于它的平衡。
B+树索引是一种多级索引,B+树结点如下图所示:
11.3.2 B+树的查询
find(V): 在B+树上找出搜索码值为V的所有记录
printAll(V): zai B+树上打印所有搜索码值为V的记录。注意repeat条件的终结条件。
B+树还可以支持查找所有搜索码的范围(L, R),此时,只要将printAll中的repeat终止条件换成L.Ki>U即可。
遍历树中从根到某个叶结点的路径:如果文件由N个搜索码值,那么这条路径的长度不超过log_[n/2](N)
例如:结点的大小一般等于磁盘块的大小,通常为4KB。如果搜索码的大小为12字节,磁盘指针的大小为8字节,那么n大约为200。如果采用保守的估计,令n=100,此时文件中搜索码共有100万个,一次查找也只需访问log_50(100万) = 4个结点。因此,查找时最多从磁盘读取4个块。通常根节点访问频繁,一般存储在缓冲区中,因此,只需要3个或更少的磁盘块读取。
B+树VS二叉树
B+树的结点很大,每个节点可含有大量的指针,因此B+树通常矮而胖;
二叉树的结点很小,只含有2个指针,通常瘦而高。查找的路径长度最长为:log_2(N)。
11.3.3 B+树的更新
插入:在函数find()中使用和查找一样的技术,我们首先找到搜索码值将出现的叶结点。然后在叶结点中插入一条新纪录(即一对搜索码值和记录指针),使得插入后搜索码仍然有序。
删除:使用和查询一样的技术,通过查找已删除记录的搜索码值,找到包含待删除项的叶结点;如果多项含有相同的搜索码值,遍历所有这些具有相同搜索码值的项,直到找到指向被删除记录的项;然后从叶结点中移除该项。该叶结点中已删除项右边的所有项左移一个位置,以便在删除该项后不会有空隙。
插入和删除的过程中需要注意分裂(split)和合并(coalesce)。
11.3.3.1 插入
插入算法:
11.3.3.2 删除
算法:
11.3.4 不唯一的搜索码
不唯一搜索码(Nonunique search key):两条或多条记录在搜索属性上存储在相同的值。
- 会导致:删除效率会比较低。假设某个特殊的搜索码值出现很多次,并且拥有该搜索码的值的一条记录将要被删除。那么删除操作需要查找很多项,该搜索有可能遍历多个叶结点。
可选解决方案:
- 方案一:创建包含原始搜索码和其他属性的复合搜索码来确保搜索码唯一。对于所有记录,该复合搜索码的值唯一。
- 通常被大多数数据库使用。
- 这个额外的属性叫做 唯一化(uniquifier)属性。
- 方案二:在该树中每个码值只存储一次,并且为该搜索码值维护一个记录指针的桶来解决不一一搜索码。
- 优点:因为只存储码值一次,所以空间利用率更高。
- 缺点:实现复杂,并且当桶比叶结点更大时,如果这些桶存储在单独的块中,会增加额外的I/O操作;此外,如果桶内的记录较多,会导致删除效率不高的问题。
11.3.5 B+树更新的复杂性
尽管B+树的插入和删除操作比较复杂,但是其需要的I/O操作较少,因为I/O操作比较费时。
B+树插入和删除的I/O次数:正比于log_[n/2](N),n是结点中指针的最大值,N是索引文件中的记录条数。
以I/O操作来衡量插入和删除操作的代价是和B+树的高度成正比的。
11.4 B+树扩展
11.4.1 B+树文件组织
索引顺序文件组织最大的缺点:
- 文件增大时性能会下降。随着文件的增大,新增的索引记录所占百分比和实际记录之间变得不协调,不得不存储在溢出块。
- 解决方案:可以通过在文件上使用B+树索引来解决查找时性能下降的问题。
B+树结构不仅可以作为索引使用,而且可以作为一个文件中记录的组织者。
B+树文件组织(B+ tree file organization):树的叶结点存储的是记录而不是指向记录的指针。
- 由于记录通常比指针大,因此一个叶结点存储的记录的数目通常比一个非叶节点存储的指针的数目少。
- 叶结点仍要求是至少半满的。
- B+树文件组织的插入和删除 与B+树索引项的插入和删除一样。
- 通过在合并和分裂时在重新分布中涉及更多的兄弟结点,可以改善B+树的空间利用率。具体如下:
- 插入和删除时引入相邻的一个结点:
- 在插入时,如果某个结点已满,系统尝试吧它的一些项重新分布到与它相邻的一个节点中,以给新项提供空间。
- 如果相邻结点也已满,系统就要分裂该结点,并在相邻的1个结点和原始结点分裂得到的2个结点,共3个结点之间均匀分配所有的项。
- 这样,每个结点至少有[2n/3]个
- 在删除时,如果结点中的项数少于[2n/3],系统就尝试从相邻的结点借入一项。
- 如果两个相邻的兄弟结点都有[2n/3]条记录,那么系统就不借入一项,而是把这个结点以及两个兄弟结点合并成2个结点。
- 在插入时,如果某个结点已满,系统尝试吧它的一些项重新分布到与它相邻的一个节点中,以给新项提供空间。
- 也可以引入更多的结点进行重新分布,例如保证每个结点至少有[3n/4]个项。
- 插入和删除时引入相邻的一个结点:
B+树索引或B+树文件组织中,相邻的叶结点可能分布在磁盘中的不同块中。
初始情况下,可以实现将一个磁盘中基本连续的块分配给树中连续的叶结点,但是随着插入和删除操作,这种顺序性逐渐丢失。
对叶结点的顺序访问也需要越来越多的磁盘寻道。===> 为了恢复顺序性,也许需要对索引进行重建。
B+树文件组织可以用于存储大对象,如:clob类型以及blob,这些对象可能比块大,甚至达到好几个G。
将其拆分成较小的记录,并组织成B+树文件组织进行存储。拆分的时候可以按序编号,这样记录的编号就可以作为搜索码。
11.4.2 辅助索引和记录重定位
一个文件组织(如B+树文件组织)可能更改记录的位置,即使记录并没有更新。
问题描述:文件重组的问题——B+树文件组织结构中的一个叶结点分裂,一些记录会移动到新的结点中。此时所有存储了那些指向重定位过的记录的指针的辅助索引都必须更新,即使记录中的值没有发生变化。此时,可能涉及到上百次的I/O操作来更新辅助索引。
解决方案:在辅助索引中,不存储指向被索引的记录的指针,而是存储主索引搜索码属性的值。于是,由于分裂导致的记录重定位就不需要更新这样的辅助索引了。
- 利用辅助索引定位记录的步骤:
- 首先利用辅助索引找到主索引搜索码的值
- 然后,用主索引来找到对应的记录。
11.4.3 字符串上的索引
在字符串属性上创建B+树索引会引起两个问题:
- 字符串是变长的
- 字符串可能会很长,会导致结点扇出降低以及相应的增加树的高度。
注意:每个结点能够容纳的记录数越多,它的扇出越高,索引树的高度也越低。不同节点可能会有不同的扇出。
前缀压缩(prefix compression)技术:可以增加结点的扇出。使用前缀压缩技术,不用再非叶结点存储整个搜索码值,只需存储每个搜索码值的前缀,使得这个前缀足以将由该搜索码值分开的两棵子树的码值分开。
- 例如:与”Siberschats”最相近的的码值是“Silas”和“Silver”,则在非叶结点中存储“Slib”就足够了,而不用存储全名“Siberschats”。
11.4.4 B+树索引的批量加载
将大量项一次插入到索引中称为索引的批量加载(bulk loading)。一种有效的执行批量加载的方法如下:
- 首先,创建有一个含有关系索引项的临时文件
- 然后,根据构建好的索引的搜索码来排序文件
- 最后,扫描排序号的文件并且将项插入到索引中
即:在将项插入到B+树之前先进行排序。其好处有:
- 在项按照排序进行插入后,所有到特定结点的项将会连续出现,并且该叶结点只需要读取一次。减少了磁盘寻道操作和I/O操作。
自底向上B+树构建(bottom-up B+ tree construction):如果B+树初始是空的,我们在插入项之前首先对其排序,然后再将其分解到块中,并保证每个块中有尽可能多的项,由此产生的块形成B+树的叶级。
如果一次性向数据库已存在的关系中添加大量的数据时,建议先删除关系上的索引,然后在插入元组后重建索引,来有效的执行批量加载技术。
11.4.5 B树索引文件
B树索引(B-tree index)和B+树索引类似,唯一的区别在于B树去除了搜索码值存储中的冗余。在B+树中,搜索码值可能重复出现在非叶结点和叶结点中,而B树中只允许出现一次。
B树的优点:可以比B+树使用更少的结点来存储索引。
B树的缺点:
- 需要在非叶子结点中添加一个人指针,指向该搜索码值对应的记录的指针或桶的指针。这会减少非叶子结点能容纳的搜索码的个数,导致其扇出变小,树的深度增加。
- 虽然对于特定的搜索,B树优于B+树(如搜索码的值恰好在非叶子结点上)。但是叶结点的数量n倍大于非叶结点的数量,n通常很大,所以这个优点微不足道。
- B树中的删除比B+树更加复杂,需要处理非叶子结点的删除。
- B树中的插入的复杂度和B+树略高
==> 结论:B树在空间上的优势不足以弥补它的缺点,所以数据库通常采用B+树数据结构。
11.4.6 闪存
闪存的价格日以下降,闪存的速度比磁盘快,称为磁盘的强大竞争者。
闪存存储也是通过块来组织的,并且B+树索引可以在闪存存储中使用,能够极大的加速索引查询速度。
闪存的唯一缺点在于不能就地写入,更新时,需要先写入一个新的数据,再擦除旧的数据。但是并不影响其性能优于磁盘。
11.5 多码访问
11.5.1 使用多个单码索引
11.5.2 多码索引
针对上述问题,可以使用复合搜索码,例如:(dept_name, salary)。
使用B+树在(dept_name, salary)构建复合索引,可以很好地执行:
- 单个复合属性上的范围查询:select ID from instructor where dept_name = "Finance" and salary < 80000;
- 一个属性上的查询:select ID from instructor where dept_name = "Finance"
复合属性的缺点:执行多个属性上的范围查询会产生大量的I/O操作
- select ID from instructor where dept_name = "Finance" and salary < 80000
11.5.3 覆盖索引
覆盖索引(covering index)存储一些属性(但不是搜索码属性)的值以及指向记录的指针。==> 优点:仅仅使用索引就能回答一些查询,甚至不需要找到实际的记录。
11.6 静态散列
顺序文件组织的一个缺点是:必须访问索引结构来定义数据,或者必须使用二分法搜索,这将导致过多的I/O操作。
散列相关的一些概念:
- 桶bucket:存储一条记录或多条记录的一个存储单位。通常一个桶就是一个磁盘块,但也可能大于或小于一个磁盘块。
- 散列函数hash function:令K表示所有搜索码的集合,令B表示所有桶地址的集合,散列函数h是一个从K到B的函数。
散列的应用:
- 散列文件组织(hash file organization): 计算所需记录搜索码值上的一个函数直接获得包含该记录的磁盘块组织
- 散列索引结构(hash index organization): 把搜索码以及与它们相关联的指针组织成一个散列文件结构
- 查找:进行基于搜索码值Ki的一次查找,计算h(Ki),找出对应的桶的地址,然后在桶上检查桶中所有的记录,确定待查找记录。
- 删除:如果待删除记录的搜索码值是Ki,计算h(Ki),然后在相应的桶上查找此记录并删除。
11.6.1 散列函数
糟糕的散列函数会使得桶的分布不均匀!==> 最坏情况会导致查找所花费的时间与文件中搜索码的数目成正比。
一个好的散列函数能够将搜索码分配到桶中,并且具有下列分布特性的散列函数:==> 可以保证一般情况下查找所需的时间是一个较小的常数。
- 分布是均匀的:散列函数从所有可能的搜索码值的集合中为每个桶分配同样数量的搜索码值
- 分布式随机的:散列值不应与搜索码的任何外部课件的排序有关。散列函数应该是随机的分布。
举例-坏的散列分布:
- 按照首字母在instructor属性上构建散列函数,共划分为26个桶。==> 分布不均匀
- 将salary从3000-4000,4000-5000,...,12000-13000之间划分出这些桶,虽然划分是均匀的,但是工资的分布不是均匀的。
11.6.2 桶溢出处理
如果桶没有足够的空间,就会发生桶溢出(bucket overflow)。
桶溢出的可能原因:
- 桶不足:桶数目的选择必须是NB>Nr/Fr,其中Nr代表将要存储的记录数,Fr表示一个同种能够存放的记录数。
- 偏斜skew:某些桶分配的记录数比其他桶多,所以即使其他桶仍然有空间,某个桶也会溢出。偏斜发生的原因有:
- 多条记录可能具有相同的搜索码
- 所选的散列函数可能会导致搜索码的分布不均
桶溢出的处理:
- 设置避让因子d:桶的数目设置为(Nr/Fr)*(1+d),通常d设为0.2
- 溢出桶(overflow bucket)和溢出链(overflow chaining):向已满的桶中插入新数据时,为已满的桶添加一个溢出桶;若溢出桶也满了,则再添加一个溢出桶。如此继续下去。一个给定桶的溢出桶可以用一个链接列表连接在一起,称为溢出链。
- 此时,查询时,除了原始桶之外,还要检查所有溢出桶的记录。
- 这种形式的散列结构成为:闭地址(close addressing)/闭散列(closed hashing)
- 缺点:在实现时就要确定一个较好的散列函数和桶的数目。==> 后续会讲动态改变桶的数目和散列函数
- 另一种方法是开地址(open addressing):它的桶集合是固定的,没有溢出链。比如:
- 使用下一个有空间的桶存储溢出的元素。——称为线性探查法(linear probing)
- 计算更多的散列函数的方法
- 注意:这种方式不使用与数据库的实现,因为插入和删除的操作十分麻烦。
11.6.3 散列索引
散列索引(hash index):将搜索码及其相应的指针组织称散列文件结构。
使用术语 散列索引 来表示散列文件结构,同时也用它表示辅助散列索引。
严格来说,散列索引只是一种辅助索引结构。散列索引从来不需要作为聚集索引结构来表示。
- 因为如果一个文件时按照散列组织的,那么就没必要再建立一个独立的索引结构。所以,也可以认为散列形式组织的文件上有一个聚集散列索引。
11.7 动态散列
静态散列技术也要求固定桶址集合B,这带来很严重的问题。大多数数据库都会随时间而变大,如果我们准备为这样的数据库使用静态散列,我们有三种选择:
- 根据当前文件的大小来选择散列函数。这种选择会使得性能随着数据库增大而下降
- 根据将来某个时刻文件的预计大小来选择散列函数。尽管这样可以避免性能下降,但是初始时会造成相当大的空间浪费
- 随着文件的增大,周期性的对散列结构进行重组。这种重组涉及到一系列问题,包括:新散列函数的选择,在文件中每条记录上重新计算散列函数,以及分配新的桶。重组是一个规模大,耗时的操作,而且重组期间必须禁止对文件的访问。
==>动态散列(dynamic hashing)技术:允许散列函数动态改变,以适应数据库增大或缩小的需要。
==> 本节介绍一种动态散列技术:可扩充散列(extendable hashing)
11.7.1 数据结构
当数据库增大或者缩小时,可扩充散列 可以通过桶的分裂或合并来适应数据库大小的变化。
优点:
- 可以保持空间的使用效率
- 由于重组每次仅作用于一个桶,因此所带来的性能开销很低,可以接受。
使用可扩充散列时,我们选择一个具有均匀性和随机性特性的散列函数h,但是该散列函数产生的值的范围很大,是b位二进制数。一个典型的是b=32。
不会为每一个散列值创建一个桶,而是在把文件记录插入文件时按需建桶。
11.7.2 查询和更新
查询
为了确定含有搜索码值Kl的桶,系统取得h(Kl)的前i个高位,然后为这个位串查找对应的表项,再根据表项中的指针得到桶的位置。
插入
要插入一条搜索码值为Kl的记录,系统按照上述相同过程进行查找,最终定位到某个桶——假定为桶j。如果该桶中有剩余空间,系统将该记录插入该桶即可;如果桶j已满,系统必须分裂这个桶并将该同种现有记录和新纪录重新分配。为了分裂该桶,系统必须首先根据散列值确定是否需要增加所使用的位数。
- 如果i=i_j,那么在桶地址表中只有一个表项指向桶j。所以系统需要增加桶地址表的大小,以容纳用于桶j分裂而产生的两个桶的指针
- 系统将i的值加1,从而使桶地址表的大小加倍
- 原表中每个表项都被两个表项替代,两个表项都包含和原始表项一样的指针。
- 现在桶地址表中有两个表项都指向桶j。此时,系统分配一个新的桶z,并让第二个表项指向新桶。
- 系统将i_j和i_z设置为i。
- 桶j的各条记录重新散列,根据前i位(注意i已经加1了)来确定该记录是在桶j中还是放到桶z中。
- 接下来,系统再次尝试插入该新纪录,通常一次尝试成功。
- 但是,如果桶j中原有的所有记录和新插入的记录具有相同的散列值前缀,该桶就必须再次分裂。因为桶j中的所有记录和新插入的记录被分配到同一个桶中。
- 如果散列函数已经精心挑选,一次插入导致两次或者两次以上分裂基本不太可能,除非大量的记录具有相同的搜索码。
- 如果桶j中所有记录具有相同的搜索码值,那么多次分裂也不能解决问题。此时==>需要溢出桶
- 如果i>i_j,那么桶地址表中有多个表项指向桶j。因此,系统不需要扩大桶地址表就能分裂桶j
- 注意,此时指向桶j的所有表项的索引前缀的最左i_j位相同。
- 系统分配一个新桶z,将i_j和i_z的值置为原始i_j+1
- 系统调整桶表地址中原来指向桶j的表项。系统让表项的前一半保持原有,指向桶j,令后一半指向新建的桶z。
- 然后,桶j中的各条记录被重新散列,分配到桶j或者桶z。
- 此时,系统再次尝试插入记录。
删除
删除的过程也首先按照前面的查找过程找到对应的桶。
系统不仅要把搜索码从桶中删除,要也要把记录从文件中删除
如果此时桶为空的,则空桶也要删除。
注意,此时某些桶可能要合并,桶地址表的大小可能减半。
与桶合并不同,若桶地址表很大,对其减半开销很大。
因此,只有桶数目减少很多时,减少桶地址表的大小才是值得的。
插入举例:
11.7.3 静态散列和动态散列的比较
可扩充散列相对于静态散列的优缺点:
优点:
- 其性能不会因为文件的增长而降低
- 空间开销是最小的,因为桶的分配时动态的,不必为将来的增长保留桶。尽管桶地址表带来了额外的开销,但是该表为当前前缀长度的每个散列值存放一个指针,因此表较小。
缺点:
- 查找时涉及一个附加层,因为系统访问桶本身之前必须先访问桶地址表。
- 这一额外的访问只对性能有一个微小的影响,虽然静态散列没有这一额外的间接层,但当它们变满后就失去了这个微小的性能优势。
11.8 顺序索引和散列的比较
文件记录组织:
- 可以用B+树组织或索引顺序组织 将 记录文件 组织成 顺序文件
- 也可以用散列来组织文件
- 也可以组织成堆文件
对关系的文件组织和索引技术做出明确的选择,数据库设计者必须考虑以下问题:
- 索引或散列组织的周期性重组代价是否可接受?
- 插入和删除的相对频率如何?
- 是否愿意以增加最坏情况下的访问时间为代价优化平均访问时间?
- 用户可能提出哪种类型的查询?
顺序索引VS散列索引:
- 散列索引更适用于等值查询:
- select A1, A2,...,An from r where Ai=c;
- 散列查询的代价是一个很小的常数,而顺序索引查找所需的时间与关系r中Ai的值的个数的对数成正比。
- 顺序索引更适合与范围查询:
- select A1, A2,...,An from r where Ai<=c1 and Ai>= c2;
- 散列查询需要读取所有的桶,而顺序索引只需找到c2,然后顺着索引读取,直至到达c1即可。
通常设计者会采用顺序索引,除非设计者预先知道不会使用范围查询。
11.9 位图索引
位图索引是一种为多码上的简单查询而设计的特殊索引。
11.9.1 位图索引结构
位图(bitmap)就是位的一个简单数组。在最简单的形式中,关系r的属性A上的位图索引(bitmap index)是由A能取的每个值建立的位图构成的。
在每个位图中都有和关系中的记录数相等数目的位,如果编号为i的记录在属性A上的值为v_j,则值为v_j的位图中的第i个位设置为1,而该位图上的其它所有位置设为0.
例如:
什么时候使用位图是有利的?
- 位图不适用的情况:例如 检索instructor_info中的女性记录
- 最简单的办法就是读取关系中的所有记录,在选择gender=f的记录
- 在这种问题上,如果使用位图,虽然它可以让我们只读取某个特定性别的记录,但是很可能这些文件中的所有块都被读到,并没有加速。
- 位图适用于:
- 多个码上的选择操作
- 例如:select* from r where gender = 'f' and income_level='L2'
- 可以使用位图,执行位图的交(Intersection,即 逻辑与)操作,得到一个新的位图。
- 注意:如果满足条件的记录所占比较少,位图能够大幅度的提高查询效率;但是如果所占比例很大,则位图可能也需要读取所有的块,此时使用扫描整个关系的代价更低。
- 例如:select* from r where gender = 'f' and income_level='L2'
- 统计满足所给条件的元组数
- 多个码上的选择操作
和实际关系相比,位图索引通常很小。
记录删除和插入的影响:
- 删除记录时会在顺序排列的记录中产生间隙,又因为移动记录或者记录号来填充间隙的代价很大。
- 解决方案:为了识别被删除的记录,可以存储一个存在位图(existence bitmap),在该位图中如果第i位为0,则表示记录i不存在,否则为1。
- 记录的插入不应该影响其他记录的顺序编号,因此,可以通过在文件尾追加或者替换被删除记录的方式完成操作。
11.9.2 位图操作的高效表现
位图的与(and)、或(or)运算的实现方式:
- 可以简单的使用一个for循环来计算
- 也可以使用计算机指令集的and指令或or指令 ==> 可以显著加快计算速度。
- 计算机指令and和or:可以用一次运算得到32位或64位的交/并结果(即一个字的大小,一个word通常有32位或54位)。
补码(not)操作:对计算机位图上的每一位取反,对于计算某个条件取反的断言很有用。
- 注意事项:
- 需要注意的是,如果某一位对于的记录被删除了,那么它的位图的值也是0,取反后却为1。
- 为了避免这种空值导致的问题情况发生,在取反后,需要与存在位图进行交操作,以确保已删除记录对应的位为0。
快速计算位图中值为1的位的数目:
- 维护一个具有256个项的数组,其中第i项 存储i的二进制表示中 值为1的个数。
- 计算一个位图中含有1的个数:首先读取位图中的每一个字节,将它对应的值作为数组的下标,从上述的数组中读取对应的值,这个值就是该字节含有的1的数目,把其加到总计数值上。
- 优点:加操作的数目将是整个元组数的1/8。
- 如果使用更大的数组(用多个字节来作为数组下标),将会得到更高的加速比,但是也需要更大的存储开销。
11.9.3 位图和B+树
对于经常出现的那些值,可以在B+树的叶结点中使用位图作为一种压缩存储机制。
- B+树索引的叶结点中,对于每个值,可以保留这个值为索引属性值的所有记录的列表。列表的每个元素可以是记录的标识符,至少为32位,通常会更多。
- 而针对一个在许多记录中都出现的值,可以通过存储一个位图,而不是记录的列表。
具体举例:
- 设一个特殊的值v_i,在1/16的关系记录中出现。令N为关系的总记录数,并假设每条记录上有一个64位的标识符来标识它。
- 如果使用位图的话,只需要1位来表示每条记录,总共需要N位
- 如果使用标识符来标识,则需要:64*N/16 = 4N位
- 可以看出:
- 如果多于1/64的记录具有相同的值,使用位图可以更节省空间
- 如果少于1/64的记录具有相同的值,那么使用标识符比使用位图更节约空间。
11.10 SQL中的索引定义
数据库系统可以自动决定创建何种索引。也可以允许程序员通过数据定义语言的命令对索引进行创建和删除。
具体声明语句取决于数据库系统的实现。
// attribute-list是作为搜索码的属性列表
create index <index-name> on <relation-name> (<attribute-list>)
// 声明该搜索码是一个候选码
create unique index <index-name> on <relation-name> (<attribute-list>)
// 删除索引
drop index <index-name>
总结
- 许多查询只涉及到文件中很少一部分记录。为了减少查找这些记录的开销,可以为存储数据库的文件创建索引。
- 索引顺序文件是数据库系统中最古老的索引模式之一。为了允许按搜索码顺序快速检索记录,记录按顺序存储,而无序记录链接在一起。为了快速的随机访问,需要建立索引结构
- 可以使用的索引类型有两种:稠密索引和稀疏索引。稠密索引为每个搜索码值都创建索引项,而稀疏索引只对某些搜索码值包含索引项。
- 如果搜索码的排序序列和关系的排序序列相匹配,则该搜索码上的索引称为聚集索引,其他的索引称为非聚集索引或辅助索引。辅助索引可以提高不以聚集索引的搜索码作为搜索码的查询的性能。但是,辅助索引增加了修改数据库的开销。
- 索引顺序文件组织的主要缺陷是:随着文件的增大,性能回下降。为了克服这个缺陷,可以使用B+树索引。
- B+树索引采用平衡树的形式,即从树根到树叶的所有路径长度相等。B+树的高度是以关系中的记录数N为底的对数成正比,其中每个非叶节点存储N个指针,N值通常约为50~100。因此,B+树比其他的平衡二叉树(如AVL树)要矮很多,故定位记录所需的磁盘访问次数也较少。
- B+树上的查询是直接而且高效的。然而,插入和删除要更复杂一些,但是仍然很有效。在B+树中,查询、插入和删除所需操作数与以关系中的记录数N为底的对数成正比。其中每个非叶结点存储N个指针
- 可以用B+树去索引包含记录的文件,也可以用它组织文件中的记录。
- B树索引和B+树索引类似。B树的主要优点在于它去除了搜索码值存储中的冗余。主要缺点在于整体的复杂性以及结点大小给定时减少了扇出。在实际应用中,系统设计者几乎无一例外的倾向于使用B+树索引。
- 顺序文件组织需要一个索引结构来定位数据。相比之下,基于散列的文件组织允许我们通过计算所需记录搜索码上的一个函数直接找出一个数据项的地址。由于设计时我们不能精确知道哪些搜索码值将存储在文件中,因此一个好的散列函数应该能均匀且随机地把搜索码值分散到各个桶中。
- 静态散列所用散列函数的桶地址集合是固定的。这样的散列函数不容易适应数据库随时间的显著增长。有几种允许修改散列函数的动态散列技术。可扩充散列是其中之一,它可以在数据库增长或缩减时通过分裂或合并桶来应付数据库大小的变化
- 也可以用散列技术创建辅助索引:这样的索引称为散列索引。为使记法简便,假定散列文件组织中用于散列的搜索码上有一个隐式的散列索引。
- 像B+树和散列索引这样的有序索引可以用作涉及单个属性且基于相等条件的选择操作。当一个选择条件中涉及多个属性时,可以取多个索引中检索到的记录标识符的交。
- 对于索引属性只有少数几个不同值的情况,位图索引提供了一种非常紧凑的表达方式。位图索引的交操作相当的快,使得它成为一种支持多属性上的查询的理想方式。