数据库系统实现(第2版)完整1——12章

书大概12章节,要求每周阅读一章并撰写读书笔记和心得,发送给我。

第一章:DBMS系统概述

1

数据库管理系统将满足:
  • 用户使用专门的数据定义语言来创建新的数据库并指定其模式(数据的逻辑结构)
  • 用户使用适当的语言查询数据
  • 大量的数据长期地进行存储
  • 数据具有持久性,从故障、多种类型的错误或者故意滥用中进行恢复。
  • 多个用户同时对数据进行访问,孤立性,原子性

关系数据库系统:关系数据库的程序员并不需要关心存储结构。查询可以用很高级的语言来表达,这样可以极大地提高数据库程序员的效率。

数据操纵语言(DML)

查询响应

  1. 查询编译器对查询进行分析和优化,得到的查询计划传给执行引擎。
  2. 执行引擎向资源管理器发出一系列对小的数据单元(通常是记录或关系的元组(tuple))的请求。
  3. 查找数据的请求被传送给缓冲区管理器。从持久地存储数据的辅助存储器(磁盘)中将数据的适当部分取到主存的缓冲区中。
  4. 缓冲区管理器和存储管理器进行通信,以从磁盘获取数据,存储管理器的任务是控制数据在磁盘上的放置和在磁盘与主存之间的移动。

事务处理:

孤立地执行的单位,一个或多个数据库操作组成一组,称作事务

1. 并发控制管理器或调度器,它负责保证事务的原子性和孤立性。

2. 日志和恢复管理器,它负责事务的待久性。

第二章:辅助存储管理

加速数据库操作的关键技术是安排好数据 , 使得当某一个磁盘块中有数 据被访问时, 大约在同时很有可能该块上的其他数据也需要被访问。

数据库中的任何修改都不能认为是最终有效的,直到该修改被存储到非易失性的辅助存储器中。

倘若一部分磁化层被以某种方式损坏,那么那些包含这个部分的整个扇 区也不能再使用。

加速对辅助存储器的访问

** 1/0开销的主导地位:块访问(磁盘1/0)次数就是算法所需要的时间的近似值, 而且应该被最小化。**
  1. 将要一起访问的块放在同一柱面上,这样我们可以经常避免寻道时间,也可能避免旋转延迟。
  2. 将数据分隔存储在几个相对较小的磁盘上而不是放在一个大磁盘上。让更多的磁头组设备分别去访问磁盘块可增加在单位时间内的磁盘块访问数量。
  3. "镜像“磁盘一把两个或者更多的数据副本放在不同的磁盘上。该策略除了可以保存数 据以备某个磁盘可能坏掉,如同将数据分隔存储几个磁盘上,可以让我们一次访问多个磁盘块。(加快读取速度,不能加快存储速度)
  4. 在操作系统、DBMS或磁盘控制器中,使用磁盘调度算法选择读写所请求的块的顺序。
  5. 预先将预期被访问的磁盘块取到主存储器中。

磁盘故障

分为间断性故障和介质损坏以及磁盘崩溃

间断性故障

磁盘块的正确内容没有被传送到磁盘控制器中。

判断:(校验和)。 每个扇区有若干个附加位,称为校验和 (checksum)

**奇偶校验**:单位/多位

关千大规模的错误,任何一个奇偶位将 检测出错误的可能性是50%, 8位都检测不 出错误的机会仅仅是1/2^8

如果我们用n个独立位作为校验码,漏掉一个错误的机会仅为1/2^n

**稳定存储的策略:**

稳定存储策略是一种数据冗余技术,用于提高数据存储的可靠性和容错能力。比如在写时发生故障,写的数据丢失,同时原数据也遭到破坏。
  1. 成对的扇区:数据被存储在成对的扇区中,每对扇区包含相同的数据内容X。这些扇区被称为“左”拷贝XL和“右”拷贝XR。
  2. 奇偶校验:每个拷贝使用额外的奇偶校验位来增强数据的完整性。
  3. 写策略
  • 步骤1:首先将数据X写入XL。如果写入过程中奇偶校验位正确,认为写入成功;否则,需要重新写入,直到成功或确定存在介质故障。
  • 步骤2:如果XL写入成功,对XR重复步骤1的操作。
  1. 介质故障处理:如果在多次尝试后,数据仍然无法写入某个扇区,可以认为该扇区存在介质故障。此时,应使用备用扇区来代替故障扇区。
  2. 读策略
  • 交替尝试读取XL和XR,直到获得一个“好”的值,即奇偶校验位正确的值。
  • 如果在预设的尝试次数内无法获得好值,可以认为数据X是不可读的。
  1. 容错能力:通过成对存储和奇偶校验,该策略能够容忍单个扇区的故障,同时保持数据的完整性。

稳定存储的错误处理

1. ** 写故障** : - 在写入数据X的过程中,如果发生系统故障(如电源断电),可能导致X在主存中丢失,同时正在写入的X的拷贝也可能被破坏。 - 系统恢复后,我们可以通过测试XL和XR的状态来确定X的值。可能的情况包括:

a) 写XL时发生故障

- <font style="color:#000000;">如果故障发生在写入XL的过程中,我们将发现XL的状态是“坏”。</font>
- <font style="color:#000000;">由于我们从未写入XR,它的状态将是“好”(除非XR同时发生介质故障,这种情况可能性很小)。</font>
- <font style="color:#000000;">这样我们可以从XR读取X的旧值,并可以将XR复制到XL,以修复XL的故障。</font>

b) 写XR后发生故障

- <font style="color:#000000;">如果故障发生在写入XR之后,我们预计XR将有状态“好”,并且我们可以从XR读取X的新值。</font>
- <font style="color:#000000;">由于XL可能包含部分新值和部分旧值,我们需要将XR复制到XL中,以确保两个扇区的数据一致性。</font>

冗余技术

将多个物理磁盘驱动器组合成一个或多个逻辑单元,以提高数据存储性能和提供容错能力的技术。

RAID1

一个数据盘,一个冗余盘。

**RAID4(奇偶块)**

多个数据盘,一个冗余盘
读操作:
+ 从数据盘读取数据块与从冗余盘读取数据块在操作上没有区别。 + 读操作可以直接从任何一个数据盘进行,因为所有数据盘上的数据块内容是一致的。
写操作:
+ 当向数据盘写入一个新的数据块时,不仅需要更改该数据盘上的块,还需要更新冗余盘上相应的奇偶校验块,以保持数据的奇偶校验一致性。
朴素的写方法:
+ 读取所有n个数据盘上相应的数据块。 + 将这些数据块进行模2加法以计算新的奇偶校验值。 + 重写冗余盘上的块以反映新的奇偶校验信息。 + 这种方法涉及n-1次数据盘的读操作,1次数据盘的写操作,以及1次冗余盘的写操作,总共是n+1次磁盘操作。
改进的写方法:
+ 只关注正在被重写的数据块i的旧版本和新版本。 + 通过取旧版本和新版本的模2加法,确定哪些位发生了变化(从0变为1或从1变为0)。 + 由于变化总是使1的总数从一个偶数变为一个奇数,只需更改冗余块中相应位置的位,即可恢复奇偶校验的一致性。 + 这种方法减少了磁盘操作次数,提高了写操作的效率。
写操作的步骤:
1. 读取要被改变的数据盘上的旧数据块。 2. 读取冗余盘上相应的奇偶校验块。 3. 将新数据块写入数据盘。 4. 根据新旧数据块的模2加法结果,重新计算并写入冗余盘的相应块。

RAID 4级别能够在单个数据盘发生故障时保护数据不丢失,并且通过奇偶校验机制实现数据的恢复。然而,每次写操作都需要更新奇偶校验信息。

RAID5

+ 旨在解决RAID 4中的写入瓶颈问题。 + RAID 5是为了提高写入性能而设计的,它通过分布式奇偶校验来平衡各个磁盘的读写负载。 + 在RAID 4中,所有的 写操作都需要访问一个专用的冗余磁盘来更新奇偶校验信息 ,这成为 系统的瓶颈 。 + RAID 5使用分布式奇偶校验,即 奇偶校验信息不是存储在一个专用的冗余磁盘上,而是分散存储在所有磁盘上 。 + 在RAID 5中,写操作不需要总是访问同一个磁盘,而是根据数据块的位置动态选择奇偶校验磁盘,从而提高了写入性能。

多个盘崩溃时的处理(RAID6)

海明码: 于检测和纠正单个位的错误(在某些情况下,可以检测两个错误并纠正一个)

【官方双语】汉明码Pa■t1,如何克服噪■_哔哩哔哩_bilibili

【官方双语】汉明码part2,优雅的全貌_哔哩哔哩_bilibili

1.盘5的位是盘1、2、3 相应位的模2和。

2.盘6的位是盘1、2、4相应位的模2和。

3.盘7的位是盘1、3、4相应位的模2和。

从任何一个数据盘中正常地读数据。冗余盘可以不予理睬。

为了写某个数据盘的一个块,我们要求那个块的新版与旧版的模2和。得出的结果与对应的冗余块进行模2和更新。

2.6 指针混写:

处理数据项在不同存储层次(如主存储器和二级存储器)之间的移动时的指针地址转换问题

指针混写是块从第二级存储器移到内存中时,将数据库地址空间转换为虚拟地址空间。

自动混写
_ 优点:_

高效性:一旦块被加载到内存,所有可识别的指针立即被混写,减少了后续操作中查找和混写指针的延迟。

简化编程:开发者无需担心指针的混写和解混写,DBMS自动处理。

缺点:

资源浪费:如果某些混写的指针从未被访问,则混写这些指针的时间和资源就被浪费了。

复杂性:需要复杂的机制来识别和定位块内的所有指针,可能涉及模式识别或额外的数据结构。

按需混写
_ 优点:_

资源节约:只在指针被实际使用时才进行混写,避免了不必要的混写操作。

灵活性:可以根据访问模式动态调整混写策略。

缺点:

延迟:首次访问未混写的指针时会有额外的混写开销。

编程复杂性:开发者需要更仔细地管理指针的混写状态,确保在需要时能够正确混写。

不混写
_ 优点:_

简单性:无需实现复杂的混写和解混写逻辑。

灵活性:指针始终以数据库地址形式存在,便于处理和跟踪。

缺点:

性能下降:每次访问数据库地址时都需要通过转换表查找对应的内存地址,增加了访问延迟。

内存管理限制:数据块无法像混写时那样被有效地固定在内存中。

混写的程序控制

优点:

灵活性:开发者可以根据具体的应用场景和需求来控制混写行为。

性能优化:对于频繁访问的数据块,可以显式地进行混写以提高性能。

缺点:

编程复杂性:需要开发者对应用的访问模式有深入的了解,并编写额外的代码来控制混写行为。

错误风险:不恰当的混写控制可能导致性能下降或资源浪费。

块返回磁盘
转换表(Translation Table)存储了内存地址(memAddr)和数据库地址(dbAddr)之间的映射关系。这种映射允许系统在需要时将内存中的地址转换回它们在数据库(即磁盘)上的原始地址,或者在数据从磁盘加载到内存时执行相反的转换。

为了加速查询过程,通常在转换表上建立索引。

索引

常见的索引结构包括散列表(Hash Table)、B树(B-Tree)或平衡树(如AVL树、红黑树)等。

散列表在提供快速查找方面具有优势,特别是对于等长键的查找。然而,散列表在处理键的删除和重新插入时可能会遇到性能问题,尤其是当发生哈希冲突时。

B树和类似的平衡树结构则更适合于需要支持范围查询和频繁更新的场景。它们通过保持树的平衡来确保查询、插入和删除操作的时间复杂度保持在对数级别。

被钉住的记录和块
如果内存中一个块当前不能安全地被写回磁盘,称它为被钉住的。

内存中B1有一个混写指针,指向B2。B2移回磁盘时,B1的指针成为悬挂指针,我们称B2是被钉住的。

我们需要解混写指向B2的所有指针。因此,对每一个有数据项在内存中的数据库地址,转换表必须记录指向内存中存在那个数据项的混写指针的位置。

为了解决悬挂指针的问题,系统需要维护一个转换表。这个表记录了每个在内存中有数据项的数据库地址,以及指向这些数据项的混写指针在内存中的位置。当某个内存块(如B2)需要被写回到磁盘时,系统会使用这个转换表来找到所有指向该内存块的混写指针,并将它们更新为新的有效值(如果B2的数据在磁盘上有了新的位置)或删除它们(如果B2的数据不再需要被引用)。

在转换表中使用附加链表
在这种方法中,转换表不仅仅存储数据库地址到内存地址的映射,还附加了一个链表来跟踪所有引用该内存地址的混写指针。这个链表实际上是附加在转换表中与特定内存地址相关联的表项上的。

当需要将一个内存块(如B2)写回磁盘时,系统会查找转换表中B2的内存地址(memAddr)对应的条目。然后,它会遍历附加在该条目上的链表,找到并更新或删除所有引用B2的混写指针。

在指针自身空间中创建链表
这种方法假设内存地址(或指针大小)比数据库地址短得多,因此有足够的空间在指针本身内部来存储额外的信息。

每个指针不再仅仅是一个内存地址,而是被扩展为一个结构体,包含至少两个部分:

a)被混写的指针(或数据库地址):指向实际数据的原始数据库地址。

b)链表指针:指向一个链表节点,该节点是包含所有引用该指针的混写指针出现的链表的一部分。

链表内容:每个链表节点包含对另一个混写指针(实际上是另一个扩展的指针结构体)的引用,这些混写指针都指向同一个内存地址。

2.7 变长数据和记录

2.7.1 具有变长字段的记录

将所有定长字段放在变长字段之前,然后我们在记录首部写人以下信息:

1.记录长度。

2.指向所有除第一个之外的变长字段起始处(即偏移量)的指针(我们知道第一个变长字段就紧跟在定长字段之后)。

2.7.2 具有重复字段的记录

方法一:
将字段F的每次出现放在一起,在记录首部放一个指针,让它指向字段F出现的第一个位置。

我们可用以下方法找到字段F出现的所有位置:令字段F的一次出现占用的字节数为L,然后在字段F的偏移量上加上L的所有整数倍数,从0开始而后L、2L、3L,依此类推。最后,我们到达F后面的字段的偏移量或记录末尾,至此停止。

方法二:
另一种表示方法是保持记录定长,而将变长部分(无论它是变长字段,还是重复次数不确定的字段)放在另一个块上。在记录本身中我们存储:

1.指向每一个重复字段开始处的指针。

2.重复次数或者重复结束处。

优点:

保持记录定长。可以更有效地对记录进行搜索,使块首部的开销最少,记录能很容易地在块内或块间移动。

缺点:

另一方面,将变长部分存储在另一个块中增加了为检査一条记录的所有部分而进行的磁盘 //0 数目。

2.7.4 不能装入一个块中的记录

如果记录能跨块,则每一条记录和记录片段需要一些额外的首部信息:

1.每一条记录或片段首部必须包含一个二进制位,指明它是否为一个片段。

2.如果它是一个片段,则它需要几个二进制位,指明它是否为它所属的记录的第一个或最后一个片段。

3.如果对同一条记录有下一个和/或前一个片段,则片段需要指向这样一些其他片段的指针。

记录片段2-a的首部包含一个指明它是片段的标记,一个指明它是记录的第一个片段的标记和一个指向下一个片段2-b的指针。

同样,2-b首部指明它是记录的最后一个片段,且有一个指向前一个片段2a的反向指针。

2.7.5 BLOB(大数据量存储)

连续存储 : 理想情况下,BLOB数据应该连续存储在磁盘上,以减少磁头移动的次数,从而提高数据读取效率。然而,随着BLOB数据量的增加,找到足够大的连续空间可能会变得困难。

链表存储: 当无法找到足够的连续空间时,BLOB数据可能被分割并存储在磁盘的多个不连续块中,这些块通过链表连接起来。这种方式虽然解决了空间分配的问题,但可能会增加数据检索时的磁盘I/O操作次数,降低性能。

多磁盘存储: 对于非常大的BLOB数据,可以考虑将其分布在多个磁盘上。这样,当进行数据检索时,可以并行地从多个磁盘读取数据块,显著提高读取速度。这种方法特别适用于需要实时处理大量数据的场景,如视频流服务。

按需传输: 数据库系统可以根据客户端的播放速度实时地传输BLOB数据块,而无需等待整个文件完全加载。这种方式极大地减少了延迟,提高了用户体验。

2.7.6列存储

列存储(Column-Oriented Storage)是一种数据库存储技术,与传统的行存储(Row-Oriented Storage)方式相对。在列存储中,数据库表中的数据不是按行存储,而是按列存储。

列存储的优势:

数据压缩: 由于同一列中的数据通常具有相似的数据类型和值域,因此可以实现高效的数据压缩。例如,在性别(gender)列中,如果只有“男”和“女”两种值,则可以使用非常少的位来表示每个值,从而大大减少存储需求。

更快的查询速度: 当查询只涉及表中的少数几列时,列存储数据库可以只读取这些列的数据,而无需读取整行数据。这减少了I/O操作的数量,从而提高了查询速度。

更好的缓存利用率: 由于相同列的数据在物理上连续存储,因此它们更有可能被缓存在一起。这提高了缓存的利用率,因为一旦某个列的数据被读入缓存,后续对该列的查询就可以直接从缓存中获取数据。

更高效的聚合操作: 列存储特别适合于执行聚合操作(如SUM、AVG、COUNT等),因为相同列的数据已经聚集在一起,可以直接进行计算而无需跨行访问。

2.8 记录的修改

2.8.1 插入

定位块->检查空间->空间足够->滑动记录->更新偏移量表->插入新记录->添加新指针(可以通过偏移量表来访问它了)

**块空间不够插入新数据**
1. 在“邻近块”中找空间

步骤

找到逻辑上或物理上紧随块B的下一个块(如块B’)。

检查块B’是否有足够的空间。

如果有,将块B中最后一个记录(或几个记录)移动到块B’的开始位置,为新记录腾出空间。

更新块B和块B’的偏移量表(如果使用)和任何必要的索引。

将新记录插入到块B的适当位置。

这种方法的好处是保持了记录的物理顺序,减少了数据碎片,但可能需要移动大量数据以腾出空间。

  1. 创建一个溢出块

当在邻近块中找不到足够的空间时,或者为了优化性能而减少数据移动,可以创建一个溢出块来存储那些在当前块中无法容纳的额外记录。

步骤

当块B没有足够的空间时,创建一个新的溢出块。

在块B的首部(或某个预定义的位置)添加一个指针,指向这个溢出块。

将原本应该插入到块B但由于空间不足而无法存储的记录放入溢出块中。

优点:减少了因插入操作而需要移动的数据量

缺点:可能会增加查询操作的复杂性,因为查询可能需要遍历多个溢出块来找到所需的记录。此外,过多的溢出块还可能导致性能下降,因为需要读取更多的磁盘块来完成查询。

2.8.2 删除

删除记录并回收空间

如果记录可以滑动,那么在删除一条记录后,可以将块中的其他记录向前滑动以填补被删除记录留下的空白。这样可以使块内的空间更加紧凑,减少碎片。在使用偏移量表的情况下,删除记录后需要更新偏移量表中的指针。

如果记录不能滑动,在块首部维护一个可用空间列表。这个列表记录了块中所有可用的空间区域及其大小。当删除一条记录时,将该记录所占用的空间区域添加到可用空间列表中。当需要插入新记录时,可以从这个列表中查找合适的空间区域。

第三章:索引结构

3.1 索引基础结构

** 索引** :它以一个或多个字段的值为输人并能快速地找出具有该值的记录。

索引使我们只需查看所有可能记录中的一小部分就能找到所需记录。建立索引的字段(组合)称为查找键。

稠密索引:为数据文件中的每个记录都创建一个索引项。这种索引方式提供了最精确的位置信息,但会占用更多的存储空间。

稀疏索引:不是为数据文件中的每个记录都创建索引项,而是选择性地为某些记录(如每个数据块的首个记录)创建索引项。稀疏索引减少了索引文件的大小,但可能需要在检索时执行额外的步骤来定位具体的数据记录。

主索引:也称为聚集索引,它决定了数据文件中记录的物理存储顺序。主索引能够直接定位到数据文件中的记录位置,因此查询效率很高。在数据库中,通常会在主键上建立主索引。

辅助索引(非聚集索引):不决定数据记录的物理存储顺序,仅提供查找键与数据记录之间的逻辑关联。辅助索引通常用于非主键列,以提高基于这些列的查询效率。

顺序文件:顺序文件是对关系中的元组按主键进行排序而生成的文件。关系中的元组按照这个次序分布在多个数据块中。

3.1.2 稠密索引

如果记录是 排好序 的,我们就可以在记录上建立稠密索引

它是这样一系列存储块:块中只存放记录的键以及指向记录本身的指针

存储索引文件比存储数据文件所需存储块要少得多。通过使用索引文件,我们每次査询只用一次 I/0操作就能找到给定键值的记录。

查找索引更快

1.索引块数量通常比数据块数量少。

索引块数量通常比数据块数量少的原因主要基于以下几点:

键与记录大小的差异:在数据库中,一个记录(record)通常包含多个字段,如姓名、地址、电话号码等,这些字段合起来可能占用相当多的存储空间。相比之下,索引中存储的仅仅是键(key)以及指向记录的指针(或地址)。键通常是记录中的一个或几个字段,因此其大小远小于整个记录。由于索引块中只存储键和指针,所以每个索引块能够存储的索引项(即键和指针的组合)数量相对较多。

压缩性:由于索引中的键通常是按顺序存储的,这允许数据在物理存储上更加紧凑。此外,如果键的类型是固定的(如整数或固定长度的字符串),那么索引块中的空间可以得到更有效的利用,因为不需要为每个键动态分配空间。

索引的稀疏性:索引不需要为数据集中的每个记录都创建一个索引项。特别是在面对大量数据时,完全索引(即每个记录都有一个索引项)可能会非常庞大且效率低下。相反,索引可以设计为稀疏的,即只在关键位置(如数据块的起始处或特定间隔)创建索引项。这样,索引文件就会比完全索引要小得多。

多层索引结构:对于非常大的数据集,通常会采用多层索引结构(如B树、B+树等)。在这些结构中,最底层的索引块(叶子节点)直接指向数据记录,而更高层的索引块则指向下一层的索引块。这种多层结构进一步减少了顶层索引块的数量,因为每个上层索引块能够覆盖多个下层索引块或数据块。

2.由于键被排序,我们可以使用二分查找法来查找K。若有几个索引块,我们只需查找log2^n 个块。

3.索引文件可能足够小,以至可以永久地存放在主存缓冲区中。要是这样的话,查找键K时就只涉及主存访问而不需执行 I/0 操作。

3.1.3 稀疏索引

稀疏索引只为数据文件的 每个存储块设一个键-指针对 ,它比稠密索引节省了更多的存储空间,但查找给定值的记录需更多的时间。

只有当数据文件是按照某个查找键排序时,在该查找键上建立的稀疏索引才能被使用,而稠密索引则可以应用在任何的査找键。

3.1.4 多级索引

![](https://img-blog.csdnimg.cn/img_convert/17dbbd30c14c902db25009a00255ef9d.png)

3.1.5 辅助索引

![](https://img-blog.csdnimg.cn/img_convert/e11ebdf6a2e05b9da6b12a89f11ef7fd.png)

有一个书架,上面放了很多书。这些书是按照某种顺序(比如书名的字母顺序)排列的,这个顺序就是书的主索引,类似于数据库中的聚簇索引。你按照这个顺序找书会很快,因为书是按照这个顺序摆放的。

但是,有时候你可能不想按照书名来找书,而是想按照作者名、出版年份或者书的主题来找。这时候,如果书架上没有额外的标记或指示来帮助你快速找到这些书,你就会很费劲。

在数据库中,这就是辅助索引的作用。辅助索引就像是书架上的标签或指示牌,它们告诉你:“如果你想找作者名是XXX的书,可以去看这里;如果你想找出版年份是YYYY的书,可以去看那里。”

具体来说,辅助索引在数据库中是一个额外的数据结构,它存储了表中某些列的值(比如作者名、出版年份等)以及这些值对应的行在表中的位置(通常是一个指向主键的指针,因为主键能够唯一标识表中的每一行)。

当你根据非主键列(比如作者名)来查询数据时,数据库会使用辅助索引来快速定位到这些列的值对应的行。首先,数据库在辅助索引中查找你指定的值(比如作者名),找到后,它会获取到对应的行位置(比如主键值),然后再根据这个位置去聚簇索引中找到完整的行数据。

需要注意的是,虽然辅助索引可以提高查询效率,但它们也会占用额外的存储空间,并且在数据发生变化(如插入、删除、更新)时,数据库需要同时更新辅助索引,这可能会增加额外的维护成本。

辅助索引总是稠密索引:辅助索引的索引项与字段值的数量是相对应的,呈现出一种“稠密”的状态。如果辅助索引是稀疏的,即只为字段的某些值建立索引项,那么就无法保证快速定位到所有相关的记录。

3.1.7 辅助索引中的间接

每个查找键K有一个键-指针对,指针指向一个桶文件,该文件中存放K的桶。从这个位置开始,直到索引指向的下一个位置,其间指针指向索引键值为K的所有记录。

辅助索引上使用间接层也有一个重要的好处:我们通常可以在不访问数据文件记录的前提下利用桶的指针来帮助回答一些查询。特别是,当查询有多个条件,而每个条件都有一个可用的辅助索引时,我们可以通过在主存中将指针集合求交来找到满足所有条件的指针,然后只需要检索交集中指针指向的记录。这样我们就节省了检索满足部分条件而非所有条件的记录所需的 I/0开销。

辅助索引求交集

3.1.8 文档检索和倒排索引

** 传统的数据库索引** :该索引的键是文档ID,而值是文档中出现的词(或词频、位置等信息)。然

倒排索引:索引的键是文档中出现的词(或称为“词条”),而值则是包含该词的文档ID列表(或称为“文档集合”)。这样,当我们想要查找包含某个特定词的文档时,就可以直接通过该词作为键来查找对应的文档ID列表,而无需遍历所有文档的索引项。

文档检索

  • 一个文档可被看成是关系 Doc的元组。这个关系有很多的属性,每个属性对应于文档可能出现的一个词。每个属性都是布尔型的–表明该词在该文档出现还是没有出现。因此,这一关系模式可以被看作:

Doc(hasCat,hasDog,…)

其中 hasCat 取值为真当且仅当该文档中至少出现一次“cat"这个词。

  • 关系 Doc 的每个属性上都建有辅助索引。不过,我们不必费心为属性值为FALSE的元组建索引项;相反,索引只会将我们带到出现该词的那些文档。也就是说,索引中只有查找键值为 TRUE的索引项。
  • 我们不是给每个属性(即每个词)建立一个单独的索引,而是把所有的索引合成一个,称为倒排索引。这个索引使用间接桶来提高空间利用率,正如3.1.7节中讨论的那样。

桶文件中指针可以是:

1.指向文档本身的指针。

2.指向词的一个出现的指针。在这种情况下,指针可以是由文档的第一个块和一个表示该词在文档中出现次数的整数构成的对。

当我们使用指针“桶”指向每个词的多次出现的时候,我们可能就会想扩展这个想法,使桶数组包含更多有关词的出现的信息。这样,桶文件本身就成了有重要结构的记录集合。

3.2 B-树

:::color2 B-树能自动地保持与数据文件大小相适应的索引层次。

对所使用的存储块空间进行管理,使每个块的充满程度在半满与全满之间。

:::

3.2.1 B-树的结构

一颗m阶的B树定义如下:

1)每个结点最多有m-1个关键字。

2)根结点最少可以只有1个关键字。

3)非根结点至少有Math.ceil(m/2)-1个关键字。((m/2)向上取整减去1)

4)每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。

5)所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同。

3.2.2 B-树的应用

B-树是用来建立索引的一种强有力的工具。它的叶结点上指向记录的一系列指针可以起到任何一种索引文件中指针序列的作用。

3.2.5 B-树的插入

1、插入一个元素时,首先在B树中是否存在,如果不存在,即比较大小寻找插入位置,在叶子结点处结束,然后在叶子结点中插入该新的元素

2、如果叶子结点空间足够,这里需要向右移动该叶子结点中大于新插入关键字的元素,如果空间满了以致没有足够的空间去添加新的元素,则将该结点进行“分裂”,将一半数量的关键字元素分裂到新的其相邻右结点中,中间关键字元素上移到父结点中(当然,如果父结点空间满了,也同样需要“分裂”操作)

3、当结点中关键元素向右移动了,相关的指针也需要向右移。如果在根结点插入新元素,空间满了,则进行分裂操作,这样原来的根结点中的中间关键字元素向上移动到新的根结点中,因此导致树的高度增加一层。

3.2.6 B-树的删除

1)如果当前需要删除的key位于非叶子结点上,则用后继key(这里的后继key均指后继记录的意思)覆盖要删除的key,然后在后继key所在的子支中删除该后继key。此时后继key一定位于叶子结点上,这个过程和二叉搜索树删除结点的方式类似。删除这个记录后执行第2步

2)该结点key个数大于等于Math.ceil(m/2)-1,结束删除操作,否则执行第3步。

3)如果兄弟结点key个数小于Math.ceil(m/2)-1,则父结点中的key下移到该结点,兄弟结点中的一个key上移,删除操作结束。

否则,将父结点中的key下移与当前结点及它的兄弟结点中的key合并,形成一个新的结点。原父结点中的key的两个孩子指针就变成了一个孩子指针,指向这个新结点。然后当前结点的指针指向父结点,重复上第2步。

有些结点它可能即有左兄弟,又有右兄弟,那么我们任意选择一个兄弟结点进行操作即可。

删除27。从上图可知27位于非叶子结点中,所以用27的后继替换它。从图中可以看出,27的后继为28,我们用28替换27,然后在28(原27)的右孩子结点中删除28。删除后的结果如下图所示。

删除后发现,当前叶子结点的记录的个数小于2,而它的兄弟结点中有3个记录(当前结点还有一个右兄弟,选择右兄弟就会出现合并结点的情况,不论选哪一个都行,只是最后B树的形态会不一样而已),我们可以从兄弟结点中借取一个key。所以父结点中的28下移,兄弟结点中的26上移,删除结束。结果如下图所示。

3.2.7 B-树的效率

B-树的效率:磁盘I/O次数、树的层数、节点的分裂与合并、以及针对查找、插入和删除操作的优化。

磁盘I/O次数

B-树的主要优势之一是它减少了磁盘I/O次数,这是因为它通过保持树的高度尽可能低来实现。在B-树中,每个节点可以包含多个键和子指针,这允许树在保持相同数量元素的情况下具有更少的层数。对于大多数数据库和文件系统应用,三层B-树足以满足需求,因为节点容量(如每块340个键-指针对)可以非常大,这取决于块的大小和键的大小。

树的层数

B-树的层数决定了查找记录所需的最大磁盘I/O次数。在三层B-树中,最多需要三次磁盘I/O(从根到叶节点,加上一次读取数据记录本身)。然而,通过缓存技术(如将根节点或某些中间节点常驻内存),这个次数可以进一步减少。例如,如果根节点被缓存,那么查找将只需要两次磁盘I/O。

节点的分裂与合并

在B-树中,节点的分裂和合并是罕见的,特别是当节点容量足够大时。当节点需要分裂时,大多数操作都局限于叶节点,这减少了对树结构整体的影响。此外,分裂和合并操作通常只涉及两个节点(一个要被分裂的节点和一个新的或已有的节点)以及它们的父节点,这进一步限制了操作的复杂性。

插入和删除

插入和删除操作可能需要额外的磁盘I/O来更新数据记录和索引本身。然而,通过适当地维护B-树的平衡(如通过旋转和重新分布节点中的键),可以最小化这些操作的影响。在某些实现中,如果删除操作导致节点中的键数低于最小要求,但预计该节点很快会再次填满,那么可能不对该节点进行合并或重新分配键,而是简单地留下该节点。

删除标记

在处理删除操作时,一种常见的策略是使用“删除标记”而不是实际从B-树中删除条目。这样做的好处是,即使记录已从数据库中删除,B-树索引仍然可以保留对该记录的引用(尽管它已被标记为删除)。这有助于维护索引的完整性,并允许通过B-树索引查询已删除记录的状态(例如,检查记录是否已被删除)。

3.3 散列表

** 散列函数** :散列函数 ( h ) 接受一个查找键(Key)作为输入,并计算出一个介于 0 到 ( B-1 ) 之间的整数,其中 ( B ) 是桶(Bucket)的数目。

桶是散列表中用于存储数据的单元。每个桶可以存储多个数据项,通常使用链表来实现。桶数组是一个序号从 0 到 ( B-1 ) 的数组,每个数组元素对应一个桶。

存储:当需要存储一个记录时,首先计算其查找键 ( K ) 的散列值 ( h(K) )。然后将该记录链接到桶号为 ( h(K) ) 的桶表中。

3.3.1 辅存散列表

![](https://img-blog.csdnimg.cn/img_convert/43271030c4433eab7fdc04c6f5840f3e.png)

有的散列表包含大量记录,记录如此之多,以至于它们主要存放在辅助存储器上,这样的散列表在一些细小而重要的方面与主存中的散列表存在区别

3.3.2 散列表的插入

![](https://img-blog.csdnimg.cn/img_convert/57eedb034832c024b2e660088c816f56.png)

查找键为K的新记录需要被插入时,计算h(K)。

如果桶号为h(K)的桶还有空间,我们就把该记录存放到此桶的存储块中。

其存储块没有空间时存储到块链上的某个溢出块中

如果桶的所有存储块都没有空间:我们就增加一个新的溢出块到该桶的链上。

3.3.5 可扩展散列表

它在简单的静态散列表结构上主要增加了:

1.为桶引人了一个间接层,即用一个指向块的指针数组来表示桶,而不是用数据块本身组

成的数组来表示桶。

2.指针数组能增长,它的长度总是2的幂,因而数组每增长一次,桶的数目就翻倍。

3.并非每个桶都有一个数据块;如果某些桶中的所有记录可以放在一个块中,那么

这些桶可能共享一个块。

4.散列函数h为每个键计算出一个K位二进制序列,该K足够大,比如32。但是,桶的数目总是使用从序列第一位或最后一位算起的若干位,此位数小于K,比如说是i位。也就是说,当i是使用的位数时,桶数组将有2^i个项。

3.3.6 可扩展散列表的插入

计算哈希值:首先,计算要插入的键的哈希值 h(K)。

定位桶数组项:使用哈希值 h(K) 的前 i 位来确定桶数组中的项。这里的 i 是当前用于索引桶数组的全局位数(global bit position)。

检查存储块空间:如果找到的存储块(B)有足够的空间来存放新记录,则直接插入新记录。

如果存储块已满(即没有空间),则需要进行分裂或桶数组扩展。

处理存储块满的情况

如果 j < i:

:::color2
将存储块 B 分裂成两个新的存储块。

根据新记录的哈希值的第 (j+1) 位来决定其归属的存储块(0 或 1)。

更新存储块的小方块中的位数为 (j+1),表示现在使用更多的位来确定存储块的成员资格。

调整桶数组中的指针,根据新记录的哈希值的第 (j+1) 位来指向正确的存储块。

如果分裂后仍然有存储块过满,则继续以更高的 j 值重复分裂过程。

:::

如果 j = i:

:::color2
桶数组的长度翻倍,并创建一个新的桶数组。

对于旧桶数组中的每个项 w,在新桶数组中创建两个项 u0 和 u1(分别通过在 w 后面添加 0 和 1 来获得)。这两个新项都指向原 w 项指向的存储块。

然后,像 j < i 的情况一样,分裂过满的存储块。

:::

**3.3.7 线性散列表**

** 可扩展散列缺点:**

1.当桶数组需要翻倍时,要花费很长的时间。

2.当桶数翻倍后,它在主存中可能就装不下了,或者把其他的一些我们需要保存在主存的数据挤出去。其结果是,一个运行良好的系统可能突然之间每个操作所需磁盘I/O开始大增。

3.如果每块的记录数很少,那么很有可能某一块的分裂比在逻辑上讲需要分裂的时间提前许多。

线性散列解决其缺点

  • 桶数n的选择总是使存储块的平均记录数保持与存储块所能容纳的记录总数成一个固定的比例,如 80%。
  • 由于存储块并不总是可以分裂,所以允许有溢出块,尽管每个桶的平均溢出块数远小于1。
  • 用来作桶数组项序号的二进制位数是1og2^n向上取整,其中n是当前的桶数。这些位总是从散列函数得到的位序列的右(低位)端开始取。
  • 假定散列函数值的i位正在用来给桶数组项编号,且有一个键值为K的记录想要插人到编号为 a,…a; 的桶中;即 a,…a 是h(K)的后i位。那么,把 a,a,…a;当作二进制整数,设它为 m。如果m<n,那么编号为m的桶存在并把记录存入该桶中。如果 n≤m<2i那么m桶还不存在,因此我们把记录存人桶m-2(i-1),也就是当我们把a1(它肯定是1)改为0时对应的桶。

**3.3.8 线性散列表的插入**

** 确定桶号:**

使用哈希函数 h(K) 计算键 K 的哈希值。

取 h(K) 序列末尾的 i 位来表示桶号 m。这里,i 是一个与当前散列表容量相关的参数,可能基于 n(当前桶的数量)的某种属性(如 log2(n) 的整数部分,但具体取决于实现)。

如果 m < n,则直接将记录插入到桶 m 中。

如果 m ≥ n,则通过 (m - 2^(i-1)) % n来计算实际的桶号,并插入记录。

处理溢出:

如果桶已满,则创建一个新的溢出块(如链表节点),将新记录存入该节点,并将此节点链接到桶的尾部。

动态扩容和桶分裂:

每次插入后,检查当前记录总数与 n 的比例是否超过了某个阈值 r/n。

如果超过,则增加散列表的容量(即增加桶的数量),并可能需要重新组织或分裂某些桶。

如果新增加的桶号的二进制表示为 1aa…a,则分裂桶号为 0aa…a 的桶。这通常意味着根据记录的后 i 位值(特别是从右数起的第 i 位)来重新分配记录到两个桶中。

i 值的调整:

当 n 超过 2^i 时,i+1。

3.4 多维索引

3.4.2 利用传统索引执行范围查询

在处理二维范围查询时,尽管分别为x和y坐标构建一维B-树索引可以高效定位单维度范围内的记录,但将它们用于二维查询时效率较低。因为即使每维索引能减少候选记录数量,交集操作后仍需访问大量数据块,导致磁盘I/O成本高昂。因此,在处理多维数据时,考虑使用如R树等多维索引结构以更直接地定位满足多维查询条件的记录,从而提高查询性能。

3.4.1 多维索引应用

1.部分匹配查询。我们指定一维或多维上的值并查找在这些维上匹配这些值的所有点。

2.范国查询。我们给出一维或多维上的范围并查找在这些范围内点的集合,或者在所表示的是形状时,查找出部分或全部形状在该范围内的形状的集合。

3.最近邻查询。我们查找与给定点最近的点。

4.where-am-查询。已知一个点,我们想知道该点所处的形状,若存在这样的形状的话。一个熟悉的例子是当你点击鼠标时,系统决定你点击的是哪个显示元素。

3.4.2传统索引进行范围查询

首先我们通过x的B-树索引得到在x范围内的所有记录的指针。然后,我们通过y的B-树索引得到在y范围内的所有记录的指针。最后求出这些指针的交集。I/O次数多。

3.5 多维数据的散列结构

3.5.1 网格文件

网格文件(Grid File)是一种用于多维数据索引的高效数据结构,它通过在各维度上划分空间为网格状区域,显著提升了多维数据查询的性能。这种划分允许将数据集划分为多个子空间(或“条状”),其中每个子空间由网格线界定,且网格线的数量和间隔在不同维度上可灵活设置,甚至在同一维度内也可不同,从而实现了对多维数据的高效索引和查询处理。

3.5.2 网格文件的查找

空间划分成的每一个区域可以被看成是散列表的一个桶 ,落人该区域的每个点的记录都存放在属于该桶的块中。如有必要,溢出块可以用来增加桶的大小。

与传统散列表中使用的一维桶数组不同,网格文件使用的桶数组的维数与数据文件的维数一样

3.5.3 网格文件的插入

我们遵循查找记录的过程并把新记录放到查找到的桶中。如果在该桶中的块有空间,那就不需要做更多的事。如果在桶中没有空间,通常有两种方法解决这个问题:

1.按需要给桶增加溢出块。

2.通过增加或移动网格线来重组结构。这种方法类似于3.3节讨论的动态散列技术,但这里还有别的问题,因为在一个维上桶的内容是相互关联的。也就是说,增加一条网格线将分裂沿该线的所有桶。因此,要选择一条对所有桶都最优的网格线也许是不可能的。例如,要是一个太大,我们也许不知是该选择对维分裂还是对点分裂,并且不会造成许多的空桶或使某些桶太满。

3.6 多维数据的树结构

1.多键索引。

2. kd-树。

3.四叉树。

4.R-树。

前三种用于点集。R-树通常用来表示区域的集合,它也可用来表示点集。

3.6.1 多键索引

假若我们有几个属性来表示我们的数据点的维,并且我们想在这些点上支持范围查询或最近邻查询。用来访问这些点的一个简单的树模式是索引的索引,或者用更一般的话来说,是一棵树,它的每一层的结点都是一个属性的索引。

这种想法如图 所示,这是两个属性的情况,“树根”是两个属性中第一个属性的索引,它可以是任何类型的常规索引,如B-树或散列表。该索引把每一个索引键值–即第一个属性的值–同指向另一个索引的指针相关联。如果V是第一个属性的一个值,那么通过键值V和它的指针找到的索引是一个指向这些点集的索引,这些点的第一个属性值是V而第二个属性为任意值。

3.6.3 kd-树

kd-树 (k维搜索树 )是把二叉搜索树推广到多维数据的一种主存数据结构。我们将先介绍这种思想,然后讨论怎样使这种思想适合存储的块模型。kd-树是一个 二叉树 ,它的内部结点有一个相关联的属性a和一个值V,它将数据点集分成两个部分:a值小于V的部分和a值大于等于V的部分。由于所有维的属性在层间交替出现,所以 树的不同层上的属性是不同的 。在一般的 kd-树中,数据点被存放在结点内,就像在二叉搜索树中一样。不过我们在开始引人这个思想时做了两个修改,以便获得块模式的有限益处:

1.内部结点只有一个属性,该属性的一个划分值和指向左、右子树的指针。

2.叶结点是块,块空间中存放着尽可能多的记录。

在kd-树中查找一个所有维都给定值的元组类似于在二叉搜索树中查找一个值。从根节点开始,根据当前节点的划分维度和查询点的对应维度值决定是向左子树还是向右子树递归查找,直到达到叶节点。如果叶节点中包含查询点,则查找成功;否则,查找失败。

3.6.4 kd-树的操作

** 插入操作**

1. 查找过程:首先,执行一个类似于查找的过程,以确定新数据点应该插入的位置。这将引导我们到一个叶节点。

2. 空间检查:如果叶节点的块中还有空间(即未达到最大容量限制),则直接将新数据点放入该块中。

3. 分裂操作:如果叶节点的块已满,则需要进行分裂操作。选择当前节点的划分维度作为分裂维度,并根据该维度上的中位数(或其他合适的值)将节点中的点分为两部分,创建两个新的子节点。

4. 创建新节点:然后,创建一个新的内部节点,其左右子节点指向这两个新创建的子节点,并设置该内部节点的划分值为分裂时使用的值。

范围查询

在范围查询中,我们可能需要根据多个维度上的范围来查找数据点。例如,查找年龄在35到55之间且薪水在8100K至200K之间的所有点。

1. 从根节点开始:根据根节点的划分维度(假设是薪水)和查询范围,确定是否进入左子树、右子树或两者都进入。

2. 递归查询:在每个子节点上重复上述过程,根据当前节点的划分维度和查询范围来决定搜索路径。

3. 收集结果:当达到叶节点时,检查该节点中的点是否满足查询条件,并将满足条件的点添加到结果集中。

4. 回溯:在回溯过程中,继续检查其他可能包含满足条件点的子树。

3.6.5 使 kd-树适合辅助存储器

为了提高kd-树在辅助存储器(如磁盘)上的效率,可以通过 内部结点的多分支聚集内部结点到块 这两种方法来解决长路径和未用空间的问题。多分支使得内部结点能够管理更多的键和指针,减少树的高度;而聚集内部结点到块则通过减少磁盘I/O次数来优化性能,因为一次磁盘访问可以处理多个内部结点。这两种方法共同作用,能够显著提升kd-树在处理大规模数据集时的效率。

3.6.7 R-树

R-树(区域树)是一种利用B-树的某些本质特征来处理多维数据的数据结构。B-树的结点有一个键的集合,这些键把线分成片段,沿着那条线的每个点仅属于一个片段, B-树因此使我们很容易地找到点 。如果我们把沿线各处的点表示成B-树结点我们就能够确定点所属的唯一子结点,在那里可以找到该点。

而R-树表示由二维或更高维区域组成的数据,我们把它称为数据区。R-树的一个内部结点对应于某个内部区域,或称“区域”,它不是普通的数据区。原则上,区域可以是任何形状虽然实际上它经常为矩形或其他简单形状。R-树的结点的键位置上含有子区域,它表示结点的子结点的内容。允许子区域有部分重叠,尽管我们希望重叠较小。

3.7 位图索引

:::color2 二进制的位数代表总共有多少数据,哪个数据中包含25,对应位为1。

第2,8个数据是25,所以25的向量为:100000001000

:::

第四章:查询执行

查询编译预览

![](https://img-blog.csdnimg.cn/img_convert/f113526a91b9b64f981b6ba39a19ff85.png)

查询分析(Analysis)

目的:将输入的查询语句(如SQL查询)转换成一种中间表示形式,即分析树(Parse Tree)或抽象语法树(AST)。这个树形结构表示了查询的语法结构,但尚未考虑查询的逻辑或物理执行方式。

过程:词法分析器(Lexer)将查询语句分解成一系列的词法单元(tokens),如关键字、标识符、常量等;然后语法分析器(Parser)根据这些词法单元构建出分析树。

查询重写(Query Rewriting)

目的:将分析树转换为初始查询计划,这个计划通常以查询的代数表达式(如关系代数表达式)的形式表示。之后,这个初始计划会被进一步优化,以产生一个预期执行时间更短的等价查询计划

过程:

逻辑优化:包括规则如谓词下推(Pushdown Predicates)、视图重写(View Rewriting)、选择投影合并(Select-Project Merge)等,以减少计算量或避免不必要的数据访问。

等价变换:寻找查询的代数等价形式,这些形式可能在执行时更高效。

物理计划生成(Physical Plan Generation)

目的:将逻辑查询计划转换为物理查询计划,这包括为每个逻辑操作符选择具体的实现算法(如哈希连接、嵌套循环连接等),并决定这些操作符的执行顺序。

过程:

操作符选择:为每个逻辑操作符(如选择、投影、连接等)选择最合适的物理实现算法。

执行顺序确定:根据数据分布、索引可用性等因素,确定操作符的最佳执行顺序。

数据传输策略:决定数据如何在操作之间传输,包括是否使用流水线(Pipeline)、内存缓冲区或磁盘等。

4.1 物理查询计划操作符介绍

4.1.1 扫描表

** 1. 表扫描(Table Scan)**

表扫描是最直接的读取数据方式,它遍历表中存储的所有数据块,读取并检查每个元组(行)以找出满足查询条件的那些。这种方式的效率通常较低,特别是在处理大型表或只有少数元组满足查询条件时。然而,在以下情况下,表扫描可能是必要的或最优的:

表中数据量很小。

没有合适的索引可以利用,或者索引的使用成本(如索引维护开销)超过了直接扫描表的成本。

查询条件涉及非索引列或复杂计算,使得索引无法有效减少搜索空间。

2. 索引扫描(Index Scan)

索引扫描利用索引来快速定位满足查询条件的元组。索引是数据库管理系统用于提高数据检索效率的数据结构,它存储了表中某些列(或列的组合)的值以及这些值对应的元组在表中的位置信息。通过索引,数据库系统可以快速缩小搜索范围,只检查那些可能包含所需数据的块或页。索引扫描可以分为几种类型:

全索引扫描:遍历索引中的所有条目,但这通常比表扫描要快,因为索引通常比表小得多。

范围索引扫描:在索引中查找一个范围内的条目。

唯一索引扫描:当索引是唯一的,并且查询条件精确匹配索引中的一个值时,这种扫描非常高效。

索引扫描的优势在于其速度,尤其是在处理大型表时。然而,索引并非没有成本。索引需要额外的存储空间,并且在数据插入、删除和更新时需要维护,这可能会降低这些操作的性能。

4.1.2 扫描表时的排序

在数据库查询处理中,排序是一个常见的操作,它可能由查询中的ORDER BY子句直接触发。

使用索引进行排序

如果排序所依据的属性(如属性a)上存在B-树索引,或者关系本身就是按照该属性的索引顺序存储的,那么可以直接通过索引扫描来获取已经排序好的数据。这种方法非常高效,因为它避免了额外的排序步骤,直接利用了索引的有序性。

内存排序

如果关系R的大小足够小,能够完全装入内存,那么可以先通过表扫描或索引扫描获取R的所有元组,然后在内存中使用排序算法(如快速排序、归并排序等)对它们进行排序。内存排序的优点是速度快,但缺点是当数据集很大时,可能无法全部装入内存,从而导致溢出到磁盘,影响性能。

多路归并排序

当关系R太大,无法全部装入内存时,就需要采用外部排序的方法。多路归并排序是外部排序的一种常用技术,它将关系R分成多个可以装入内存的部分(称为“块”或“段”),对每个部分在内存中进行排序,然后将排序后的部分写入到外部存储(如磁盘)上。最后,使用多路归并算法将这些排序后的部分合并成一个完整的有序关系。

4.1.4 衡量代价的参数

** B:** 当描述一个关系R的大小时,绝大多数情况下,我们关心包含R的所有元组所需的块的数目。这个块的数目表示为B(R)。

T:有时候,我们也需要知道R中的元组的数目,我们将这个数量表示为T®。

V:最后,我们有时候希望参考出现在关系的一个列中的不同值的数目。如果R是一个关系,它的一个属性是。那么V(R,a)是R中a对应列上不同值的数目。

4.1.6 实现物理操作符的迭代器

** 1.0pen()。** 这个方法启动获得元组的过程,但并不获得元组。

2.GetNext()。这个方法返回结果中的下一个元组

3. Close()

/注意,如果S已消耗完,s.GetNext将会返回 NotFound,对 GetNext 来说这也是正确的动作

4.2 一趟算法

我们怎样执行逻辑查询计划中的每个单独的步骤(例如,连接或选择)?关于各种操作符已提出了很多算法,

我们可以将操作符算法按照难度和代价分成三种“等级”:

a)一些方法仅从磁盘读取一次数据,这就是一趟(one-pass)算法。它们通常要求操作的至少一个操作对象能完全装人内存.

b)一些方法处理的数据量太大以至于不能装入可利用的内存,但又不是可想象的最大的数据集合。这些两趟算法的特点是首先从磁盘读一遍数据,用某种方式处理,将全部或绝大部分写回磁盘,然后在第二趟中为了进一步处理,再读一遍数据。

c)某些方法对处理的数据量没有限制。这些方法用三趟或更多趟来完成工作,它们是对两趟算法的自然的递归的推广。

操作符分为三大类:

1.一次单个元组,一元操作。我们一次可以读一个块,使用内存缓冲区,并产生我们的输出。

2.整个关系,一元操作。这些单操作对象的操作需要一次从内存中看到所有或大部分元组因此,一趟算法局限于大小约为M(内存中可用缓冲区的数量)或更小的关系。

3.整个关系,二元操作。其他所有的操作可以归为这一类:并、交、差、连接和积的集合形式以及包形式。我们将发现如果要用一趟算法,那么这类操作中的每一个都要求至少一个操作对象的大小限制在 M 以内。

4.2.1一次单个元组操作的一趟算法

我们一次读取R的一块到输人缓冲区,对每一个元组进行操作,并将选出的元组或投影得到的元组移至输出缓冲区。

如果R是聚集的,代价就是B,如果R不是聚集的,代价就是T。

B:包含R的所有元组所需的块的数目。这个块的数目表示为B®。

T:R中的元组的数目,我们将这个数量表示为T®。

4.2.2 整个关系的一元操作的一趟算法

** 一元操作(消除重复**![](https://img-blog.csdnimg.cn/img_convert/04591252694d27f75eebd61fcae2036b.png)** ,分组**![](https://img-blog.csdnimg.cn/img_convert/a533a14afc1193b114b47b3f924e7887.png)** )**

消除重复

一次一个地读取R的每一块,但是对每一个元组,我们需要判定:

1.这是我们第一次看到这个元组,这时将它复制到输出。

2.我们从前见过这个元组,这时不必输出它。

为支持这个判定,我们需要为见过的每一个元组在内存中保存一个备份。用一个内存缓冲区保存R的元组的一个块,其余的M-1个缓冲区可以用来保存目前为止我们见过的每个元组的一个副本。

分组

  • 对 MIN(a)或MAX(a)聚集来说,分别记录组内迄今为止见到的任意元组在属性a上的最小或最大值。每当见到组中的一个元组时,如果合适,就改变这个最小值或最大值。
  • 对于任意 COUNT聚集来说,为组中见到的每个元组加1。
  • 对SUM(a)来说,如果a不为NULL的话,在它的组里扫描到的累加值上增加属性a的值。
  • AVG(a)的情况复杂。我们必须保持两个累计值:组内元组个数以及这些元组在a上的值的和。二者的计算分别与我们为COUNT和SUM聚集所做的一样。当R中所有元组都被扫描后,我们计算总和和个数的商以得到平均值。

4.2.3 二元操作的一趟算法(并、交、差、积和连接)

** B:包 S:集合**

1. 集合并(Union)

操作:合并两个集合S和R中的所有不重复元素。

实现:将S读入内存并使用查找结构(如哈希表)存储,然后遍历R的每一块,对于R中的每个元素,如果它不在S中,则将其复制到输出。

2. 集合交(Intersection)

操作:找出同时存在于S和R中的元素。

实现:同样将S读入内存并使用查找结构,然后遍历R的每一块,对于R中的每个元素,如果它也在S中,则将其复制到输出。

3. 集合差(Difference)

操作:找出存在于S但不在R中的元素(S-R),或存在于R但不在S中的元素(R-S)。

实现:对于S-R,遍历R并删除S中与R相同的元素,最后输出S中剩余的元素。对于R-S,遍历R并仅输出不在S中的元素。

4. 包交(Bag Intersection)

操作:类似于集合交,但保留元素出现的次数。

实现:为S中的每个元素维护一个计数,遍历R时,如果元素在S中存在且计数大于0,则输出该元素并减少计数。

5. 包差(Bag Difference)

操作:类似于集合差,但保留元素出现的次数。

实现:对于S-R,遍历R并减少S中对应元素的计数,最后输出计数大于0的元素及其剩余次数。对于R-S,遍历R,如果元素不在S中或S中计数为0,则输出元素(可能带计数)。

6. 积(Cartesian Product)

操作:将S和R中的每个元素组合起来。

实现:将S读入内存,然后遍历R的每一块,将R中的每个元素与S中的每个元素组合后输出。

7. 自然连接(Natural Join)

操作:基于两个关系的公共属性连接它们。

实现:将S读入内存并使用公共属性作为查找键构建查找结构,然后遍历R的每一块,对于R中的每个元素,使用查找结构找到S中与之匹配的元素,并输出连接后的结果。

4.3 嵌套循环连接

4.3.1 基于元组的嵌套循环连接

![](https://img-blog.csdnimg.cn/img_convert/8dcddfe3d358fe5a0d9fbe2cc76cf57b.png)

如果我们不注意关系R和S的块的缓冲方法,那么这种算法需要的磁盘I/0可能多达T®T(S)。

当我们可以使用R的连接属性上的索引来查找与给定的8元组匹配的R元组时,这样的匹配不必读取整个关系R。

执行内层循环时,要尽可能多地使用内存,以减少磁盘0的数目。

4.3.3 基于块的嵌套循环连接算法

1.对作为操作对象的两个关系的访问均按块组织。

2.使用尽可能多的内存来存储属于关系S的元组,S是外层循环中的关系。

第1点确保了当在内层循环中处理关系R的元组时,我们可以用尽可能少的磁盘I/0来读取R。

第2点使我们不是将读到的R的每一个元组与S的一个元组连接,而是与能装入内存的尽可能多的S元组连接。

σ® - 选择(Selection)π® - 投影(Projection)

♾️:连接

4.4 基于排序的两趟算法

4.4.1 两阶段多路归并排序

两阶段多路归并排序(TPMMS)是一种针对大数据集进行排序的有效方法,特别是在处理无法一次性装入内存的数据集时。

第一阶段:分割和局部排序

分割:将大数据集R分割成多个较小的部分(块),每个部分大小适中,可以单独放入内存进行排序。

局部排序:使用内存中的排序算法(如快速排序、归并排序等)对每个块进行排序,并将排序后的结果写回到外存(如硬盘)。每个块排序后形成一个有序的子表。

第二阶段:全局归并

读取和归并:从外存中读取这些有序的子表到内存中,并使用多路归并算法将它们合并成一个大的有序表。

多路归并:同时处理多个有序子表,每次从所有子表中选出最小(或最大)的元素,并将其放入结果集中。这个过程中,可能需要多次从外存读取子表数据到内存。

4.4.2 利用排序去除重复

** 第一趟:分割和局部排序**

与TPMMS的第一阶段相同,

第二趟:去重和归并

去重:对于每个输入块,算法读取第一个未考虑的元组t,并将其复制到输出块中。然后,它检查输入块中剩余的元素,如果发现有与t相同的元素,则将它们从输入块中删除(或标记为已处理,实际删除可能不是必需的,具体取决于实现方式)。这样,每个唯一的元组只会被复制到输出块一次。

归并:由于输入块已经是有序的,因此可以确保在复制过程中,输出块中的元素也是有序的。随着输入块的读取和去重,算法会不断地将唯一的元组添加到输出块中。

性能分析

磁盘I/O:与TPMMS相同,该算法的总磁盘I/O次数也是3B®,其中B®是数据集的块数。这是因为每个块在第一趟中需要被读入和写出一次,在第二趟中可能再次被读入(用于去重和归并),并且结果可能还需要被写出(尽管在某些情况下,这个结果可能直接用于后续处理而不需要写回磁盘)。

4.4.3 利用排序进行分组和聚集

** 第一趟:分割、排序和写入**

读取和排序:将数据集R的元组按批次读取到内存中,每次读取M个块。使用分组属性作为排序关键字,对每个内存中的块进行排序。

写入磁盘:将每个排好序的子表(块)写回到磁盘上。这样,每个子表都是按分组属性有序的。

第二趟:归并、分组和聚集

初始化缓冲区:为每个子表分配一个主存缓冲区,并将每个子表的第一个块加载到其对应的缓冲区中。

查找最小分组:在所有缓冲区的可用元组中,反复查找排序关键字(即分组属性)的最小值。这个最小值确定了下一个要处理的分组。

分组和聚集:

准备聚集:为每个新的分组准备一个空白的聚集结果列表或数据结构。

遍历和累计:遍历所有缓冲区中的元组,检查每个元组的分组属性是否与当前处理的分组相匹配。如果匹配,则根据需要对元组中的其他属性进行聚集操作(如计数、求和等)。

更新聚集结果:将每个匹配的元组的贡献累加到当前分组的聚集结果中。

替换空缓冲区:如果一个缓冲区的所有匹配元组都已处理完毕(即缓冲区为空或剩余元组不属于当前分组),则用同一子表中的下一个块替换它(如果有的话)。

输出分组和聚集结果:当不再有排序关键字为当前分组的元组时,输出一个包含分组属性和对应聚集值的元组。

重复:重复步骤2-5,直到所有分组都被处理完毕。

4.4.4 基于排序的并算法

** 第一趟:创建排序子表**

将关系R和S的元组分别读取到内存中,并使用排序关键字(通常是元组本身,因为并集操作不需要特定的排序属性)进行排序。

将排好序的元组块写回到磁盘上,形成排序子表。这些子表将用于第二趟的归并操作。

第二趟:归并和输出

为R和S的每个子表分配一个内存缓冲区,并用每个子表的第一块数据初始化这些缓冲区。

重复地在所有缓冲区中查找并比较剩余的第一个元组t。将t复制到输出缓冲区(或直接输出到最终结果中,如果输出缓冲区已满或接近满),并从所有包含t的缓冲区中删除t的副本(在集合的情况下,每个元素至多有两个副本,分别来自R和S)。

当输入缓冲区变空或输出缓冲区变满时,根据TPMMS算法的策略进行处理:从磁盘上读取新的块到输入缓冲区,或将输出缓冲区的内容写回磁盘,并清空输出缓冲区以继续接收新数据。

4.4.5 基于排序的交和差算法

** 集合与包交算法**

集合交:当处理到所有缓冲区中最小的元组t时,如果t同时在关系R和S的缓冲区中都存在,则输出t。

由于是集合操作,不考虑t在R和S中出现的次数,只关心是否存在。

包交:对于包交,需要计算t在R和S中出现的最小次数,并输出该次数。

如果t在任一关系中未出现(即计数为0),则不输出t。

集合与包差算法

集合差 R-S:当处理到所有缓冲区中最小的元组t时,如果t在R的缓冲区中存在但在S的缓冲区中不存在,则输出t。由于是集合操作,不关心t在R中出现的具体次数。

包差 R-S:对于包差,需要计算t在R中出现的次数减去在S中出现的次数,并输出该差值。如果差值小于或等于0(即t在S中出现的次数至少等于在R中出现的次数),则不输出t。当处理一个块且该块中所有剩余的元组都是t时,需要继续读取下一个块来计算t的准确出现次数。

4.4.6 基于排序的一个简单的连接算法

给定两个要连接的关系R(X, Y)和S(Y, Z),其中Y是连接属性,且有一个大小为M的内存块限制用于缓冲区。算法的目标是计算这两个关系的连接结果。

1. 排序:

○ 使用Y作为排序关键字,对关系R和S分别应用2PMMS(两趟多路归并排序)算法进行排序。排序后,具有相同Y值的元组在各自的关系中会相邻。

2. 归并和连接:

○ 使用两个缓冲区,一个用于存储R的当前块,另一个用于存储S的当前块。

○ 重复执行以下步骤,直到R和S的所有元组都被处理完:

a. 在当前R和S的块的前端查找连接属性Y的最小值y。

b. 如果y在另一个关系的前部没有出现(即,当前R块的最小Y值大于当前S块的最大Y值,或反之),则删除当前块中具有排序关键字y的元组(或跳过它们,因为它们无法参与当前连接),并尝试从磁盘加载下一个块到相应关系的缓冲区中。

c. 如果y在两个关系中都出现,则:

■ 从R和S中读取尽可能多的块,直到确定每个关系中都不再有y的副本。在这个过程中,可以使用多达M个缓冲区来存储具有相同Y值的元组。

■ 输出通过连接R和S中具有共同的Y值y的元组所能形成的所有元组。这通常涉及嵌套循环遍历具有相同Y值的元组对。

d. 如果一个关系在内存中已没有未考虑的元组(即,其缓冲区为空),则重新从磁盘加载该关系的下一个块到缓冲区中。

4.4.8 一种更有效的基于排序的连接

** 排序阶段:**

使用连接属性Y作为排序关键字,分别对关系R(X, Y)和S(Y, Z)进行排序。

将排序后的数据分割成多个子表(或块),每个子表的大小通常受限于内存中的可用缓冲区大小。这里假设总共有不超过M个子表,每个子表可以被完全加载到内存中。

初始化缓冲区:

为每个子表分配一个缓冲区(总共不超过M个缓冲区),并将每个子表的第一块加载到对应的缓冲区中。

归并和连接阶段:

重复执行以下步骤,直到所有子表都被完全处理:

a. 在所有子表的当前块中查找具有最小Y值的元组。

b. 一旦找到具有最小Y值y的元组,就识别出关系R和S中所有具有此Y值的元组。

c. 使用找到的元组进行连接操作,并输出所有可能的连接结果。这通常涉及到对两个关系中具有相同Y值的元组进行嵌套循环遍历。

d. 如果一个子表的当前块中的所有元组都已被处理,则将该子表的下一个块从磁盘加载到其对应的缓冲区中。如果所有块都已被处理,则可能需要重新排序或合并剩余的子表块。

4.5 基于散列的两趟算法

基于散列的算法在处理大数据集时提供了一种有效的方法来减少内存使用并提高操作效率。这些算法通过选择适当的散列关键字, 将大量数据分散到有限数量的桶(或称为哈希桶)中,从而允许算法一次处理一个桶或具有相同散列值的一对桶。

4.5.1通过散列划分关系

** 初始化:**

设定散列函数h,该函数将关系R中的整个元组t作为输入,并输出一个介于0到M-1之间的整数,表示桶的索引。

准备M个缓冲区(或磁盘上的块),每个缓冲区对应一个桶。注意,实际上只有M-1个桶用于存储数据,而第M个缓冲区用作临时存储,以便在最后一个桶填满时可以继续加载数据。

遍历关系R:

遍历关系R中的每个元组t。使用散列函数h计算元组t的桶索引h(t)。将元组t复制到对应桶的缓冲区中。

缓冲区管理:

如果某个桶的缓冲区已满,将该缓冲区的内容写入磁盘上的相应块中。

为该桶初始化一个新的缓冲区,以便继续存储后续的元组。

处理最后一个桶:

在遍历完关系R的所有元组后,最后一个桶(可能包括第M个缓冲区中暂存的元组)可能并未填满。如果最后一个桶的缓冲区不为空,将其内容写入磁盘上的相应块中。

4.5.2 基于散列的消除重复算法

** 散列划分:** 使用散列函数h将关系R的元组划分到M-1个桶中。每个桶与一个缓冲区或磁盘块相关联,用于存储散列到该桶的元组。如果某个桶的缓冲区满了,就将其内容写入磁盘上的一个新块,并为该桶初始化一个新的缓冲区。

写入磁盘:遍历完关系R后,将所有非空桶的缓冲区内容写入磁盘上的块中。

重复消除:对每个桶中的数据进行重复消除。由于每个桶的大小相对较小(假设每个桶都能装入内存),因此可以使用一趟算法(如4.2.2节中讨论的方法)来去除重复元组。

优点:

减少了内存使用,因为每个桶都可以独立地装入内存处理。

提高了处理速度,因为可以并行处理多个桶。

适用于大数据集,因为可以通过增加桶的数量来扩展处理能力。

4.5.3 基于散列的分组和聚集算法

** 散列划分:** 使用依赖于分组属性的散列函数h将关系R的元组划分到M-1个桶中。每个桶收集具有相同散列值的元组,这些元组在分组属性上可能相同或相似。

写入磁盘:

如果某个桶的缓冲区满了,就将其内容写入磁盘上的一个新块,并为该桶初始化一个新的缓冲区。最终,所有非空桶的内容都需要被写入磁盘,以便后续处理。

分组和聚集:遍历每个桶,对每个桶内的元组进行分组。由于桶的大小可能较大,但分组是基于散列值的,因此同一分组的所有元组理论上应该都在同一个桶内。

对每个分组应用聚集函数(如求和、平均值、最大值、最小值等)。

合并结果:

将来自不同桶的聚集结果合并成一个完整的结果关系。这通常不需要额外的磁盘I/O,除非结果关系本身太大而无法完全装入内存。

4.5.4 基于散列的并(∪)、交(∩)和差(-)算法

** 散列划分:** 使用相同的散列函数h将关系R和S的元组分别散列到M-1个桶中。这意味着对于R,我们有桶R₀, R₁, ..., R_{M-1};对于S,我们有桶S₀, S₁, ..., S_{M-1}。

写入磁盘:如果某个桶的缓冲区满了,就将其内容写入磁盘上的一个新块。最终,所有非空桶的内容都需要被写入磁盘,以便后续处理。

执行集合操作:

并(∪):对于每个i,计算桶R_i和S_i的并集,并将结果输出到结果关系的相应桶中。注意,由于使用了相同的散列函数,任何在两个关系中都出现的元组都会被散列到相同的桶中,从而避免了在结果中引入重复。

交(∩):对于每个i,计算桶R_i和S_i的交集,并将结果输出到结果关系的相应桶中。

差(-):对于每个i,计算桶R_i中但不在S_i中的元组(即R_i - S_i),并将结果输出到结果关系的相应桶中。注意,如果还需要计算S - R,则需要额外处理。

4.5.5 散列连接算法

为了计算关系R(X, Y)和S(Y, Z)的连接,我们可以使用基于散列的两趟算法。这个算法的核心思想是利用连接属性Y作为散列关键字,将R和S的元组分别散列到多个桶中。然后,对每个对应的桶对执行一趟连接操作,最终将所有桶的连接结果合并起来得到最终的结果关系。

散列划分:使用连接属性Y作为散列关键字,将R和S的元组分别散列到M个桶中。结果得到桶R_0, R_1, …, R_{M-1}和桶S_0, S_1, …, S_{M-1}。

写入磁盘:将每个桶的内容写入磁盘,以便后续处理。

执行连接操作:对每对对应的桶(R_i 和 S_i)执行一趟连接操作。由于连接属性Y被用作散列关键字,因此如果R中的元组t_R能与S中的元组t_S连接,则它们必然位于具有相同i值的桶中。在一趟连接过程中,算法会读取桶R_i和S_i的内容到内存中,执行连接操作,并将结果输出到结果关系的相应部分。

合并结果:将来自不同桶的连接结果合并成一个完整的结果关系。这通常涉及读取每个桶的连接结果,并将它们组合起来。

磁盘I/O:散列连接算法大致需要3(B® + B(S))次磁盘I/O操作,其中B®和B(S)分别是关系R和S的块数。这些操作包括读取R和S的块到桶中、将桶写入磁盘(如果必要)、以及读取桶对以执行连接操作。

4.5.6 节省一些磁盘 I/O(混合散列连接)

** 桶的分配与内存使用:**

假设有两个关系R和S,其中S较小。我们创建k个桶,但k的数量远小于可用的内存M(以块为单位)。选择m个桶(m < k)完全保留在内存中,以存储S的元组。这些桶的选择基于它们预期的大小和内存的限制。对于剩下的k-m个桶,每个桶只保留一个块在内存中,其余部分写入磁盘。

第一趟处理:

当读取S的元组时,将它们分配到相应的桶中。m个桶完全保留在内存中,而k-m个桶的剩余部分写入磁盘。

当读取R的元组时,也进行类似的分配。但此时,对于R的每个桶,如果它对应的S桶在内存中,则直接进行连接;如果S桶在磁盘上,则将R桶的当前块写入内存中的临时空间,以便后续处理。

第二趟处理:

读取所有写入磁盘的桶(包括S和R的桶),并对它们进行连接。重要的是,对于那些在第一趟中已经在内存中完成连接的S桶和R桶对,不需要在第二趟中再次连接。

节省磁盘I/O的关键

减少寻道时间和旋转延迟:通过将多个块连续写入磁盘,可以减少磁盘的寻道次数和旋转延迟,从而提高I/O效率。

避免不必要的读写:通过在内存中直接连接部分桶,可以避免将这些桶的内容写入磁盘后再读取的额外I/O

要最大化节省的磁盘I/O,需要使m/k的比率尽可能大,同时满足内存限制。

基于排序和基于散列的区别

** 大小依赖性的差异:**

基于散列的算法在处理二元操作时(如比较、合并等),其性能或空间需求通常依赖于两个操作对象中较小的一个的大小。这是因为散列通常通过映射较小的数据元素到固定的桶(或位置)中来工作,避免了直接处理整个数据集的大小。

相比之下,基于排序的算法通常需要处理整个数据集,或至少是数据集的一个显著部分,其性能往往与数据集的总大小直接相关。

有序输出的能力:

基于排序的算法天然能够产生有序的结果集,这对于需要排序输出的应用非常重要。排序后的数据还可以用于进一步的查询或分析,如二分查找、范围查询等。

基于散列的算法则不直接提供排序功能,其输出通常是无序的。如果需要排序,则必须额外进行排序操作。

桶大小的限制:

基于散列的算法依赖于固定大小的桶来存储数据。如果数据分布不均,可能导致某些桶过载而其他桶空闲,影响性能。此外,桶的数量通常受限于可用的存储空间或内存大小。

特别是在处理具有少量不同值的场景(如group-by操作中的少量分组)时,桶的利用效率可能较低。

磁盘I/O效率:

在基于排序的算法中,通过合理组织磁盘上的数据块,可以最小化磁盘I/O操作,提高性能。特别是当排序后的数据可以连续存储在磁盘上时,可以减少磁头移动和旋转延迟时间。

基于散列的算法在磁盘I/O方面可能不那么高效,除非能够精心组织桶的存储位置以减少磁盘访问时间。

批量读写优化:

当排序子表的数量远小于可用内存块(M)时,基于排序的算法可以通过批量读写磁盘块来减少I/O操作次数和延迟。

类似地,在基于散列的算法中,如果桶的数量可以精心选择以匹配磁盘块的布局,则可以实现类似的性能优化。

4.6 基于索引的算法

通过在关系的一个或多个属性上创建索引,可以极大地加速数据检索、选择和连接等操作。基于索引的算法利用这些索引来快速定位数据,从而显著提高查询效率。

4.6.1聚簇和非聚簇索引

** 聚簇(Clustering)**

在数据库管理系统中,聚簇通常指的是将表中的数据物理地存储在一起,以便基于某个特定的顺序或属性来优化查询性能。当关系的元组被紧缩到能存储这些元组的尽可能少的块中时,我们称这个关系是被聚簇的。聚簇可以是基于整个表(即整个关系)的,也可以是基于表中某个或某些属性的。

聚簇索引(Clustered Index)

聚簇索引是一种特殊的索引,它不仅定义了表中数据的逻辑顺序,还决定了数据的物理存储顺序。在具有聚簇索引的表中,索引键的值决定了数据行在磁盘上的物理位置。这意味着,当你通过聚簇索引来查询数据时,数据库可以直接定位到数据的物理位置,并快速读取数据。

非聚簇索引(Non-Clustered Index)

与聚簇索引不同,非聚簇索引不定义表中数据的物理存储顺序。相反,它提供了一个索引结构,该结构包含索引键和指向表中相应数据行的指针。这意味着,当你通过非聚簇索引来查询数据时,数据库首先使用索引来快速定位到数据行的指针,然后通过该指针访问实际的数据行。

4.6.2 基于索引的选择

** 聚簇索引下的选择操作**

当属性a上的索引是聚簇的时,数据本身就是按照索引键(即属性a的值)的顺序存储的。这意味着,如果我们想要选择所有a=v的元组,我们可以直接通过索引快速定位到这些元组在磁盘上的位置,并读取它们所在的块。

然而,实际的磁盘I/O次数可能会受到几个因素的影响:

索引不在内存中:索引本身可能不完全驻留在内存中,因此可能需要从磁盘读取索引块。

块分裂:即使所有满足条件的元组都可以放入少量的块中,这些元组也可能因为块分裂而分布在多个块中,特别是如果它们不是从块的开始位置开始的。

块填充和预留空间:关系R的块可能没有被完全填满,或者为了未来的插入操作而预留了空间。这会导致需要读取更多的块来检索所有相关的元组。

整数取整:如果B®/V(R,a)不是整数,我们需要向上取整以确保所有相关的块都被读取。

非聚簇索引下的选择操作

在非聚簇索引的情况下,索引仅仅提供了指向数据行指针的列表,而数据本身并不是按照索引键的顺序物理存储的。因此,当我们通过非聚簇索引查找满足a=v的元组时,每个匹配的元组都可能位于不同的块中。这导致我们需要读取更多的块来检索所有相关的元组,因为索引只能告诉我们每个元组的位置,而不能保证它们会聚集在一起。

**4.6.3 使用索引的连接**

** 自然连接 R(X, Y) ⨝ S(Y, Z)**

在自然连接中,我们假设两个关系R和S分别具有属性集X和Y的交集,以及各自的额外属性集(Y对于R,Z对于S)。连接操作基于这些共同的属性(在这个例子中是Y)来合并元组。

索引的使用

假设S在属性Y上有一个索引。这个索引可以极大地加速连接过程,因为它允许我们快速定位S中所有与R中当前元组在Y属性上相匹配的元组。

磁盘I/O数量的分析

读取R的元组:

如果R是聚簇的,我们需要读取B®个块来获取R的所有元组。

如果R不是聚簇的,可能需要读取多达T®个块(即R中元组的总数)。

对于R中的每个元组,查找S中的匹配元组:

使用S在Y属性上的索引,我们可以快速定位到S中所有Y值匹配的元组。

如果索引是非聚簇的,那么对于R中的每个元组,我们平均需要读取T(S)/V(S,Y)个S的元组(这里V(S,Y)是S中Y属性不同值的数量)。因此,总的磁盘I/O次数大约为T®T(S)/V(S,Y)。

如果索引是聚簇的,那么我们可以直接通过索引访问到物理上连续存储的元组,从而减少磁盘I/O。在这种情况下,我们大约需要T®B(S)/V(S,Y)次磁盘I/O(这里假设每个块可以容纳多个具有相同Y值的元组)。注意,如果B(S)/V(S,Y)小于1,则至少需要一个磁盘I/O来读取包含至少一个匹配元组的块。

4.6.4 使用有序索引的连接

当索引以有序形式(如B-树索引)存在时,我们可以利用这些索引来优化连接操作。有序索引允许我们直接按照排序顺序遍历关系中的元组,而无需显式地对整个关系进行排序,从而节省了大量的计算资源和时间。

Zig-Zag 连接

当两个关系R(X, Y)和S(Y, Z)在共同的属性Y上都拥有有序的索引时,我们可以采用一种高效的连接方法,称为Zig-Zag连接。这种方法的核心思想是同时遍历R和S的索引,并比较当前元组的Y值,以找到所有匹配的元组对。

初始化:设置两个指针,一个指向R的索引的起始位置,另一个指向S的索引的起始位置。

比较与遍历:比较两个指针当前指向的元组的Y值。如果Y值相等,则这两个元组构成一个连接对,输出它们,并将两个指针都向前移动。如果R的Y值小于S的Y值,则将R的指针向前移动。如果R的Y值大于S的Y值,则将S的指针向前移动。重复上述步骤,直到至少一个索引被完全遍历。

4.7 缓冲区管理

4.7.1 缓冲区管理结构

** 1. 直接控制内存的缓冲区管理器**

在这种模式下,DBMS的缓冲区管理器直接管理内存中的缓冲区,负责分配和回收内存空间给这些缓冲区。

2. 使用虚拟内存的缓冲区管理器

DBMS的缓冲区管理器在虚拟内存中分配缓冲区,而不是直接管理物理内存。操作系统负责将虚拟内存映射到物理内存,并根据需要在物理内存和磁盘的交换空间之间移动数据块。

4.7.2 缓冲区管理策略

> ** 最近最少使用(LRU)** > > ** 先进先出(FIFO)** > > ** “时钟”算法(第二次机会)** >

4.8 使用超过两趟的算法

4.8.1 基于排序的多趟算法

** 当关系R的大小(以块数B(R)衡量)可以直接装入M个内存缓冲区时:**

读取数据:将整个关系R读入内存。

排序:在内存中使用任何高效的排序算法(如快速排序、归并排序等)对R进行排序。

写回磁盘:将排序后的关系R写回到磁盘上。

当关系R的大小超过了M个内存缓冲区能够容纳的范围时:

分组:将R的块分成M个组,即R₁, R₂, …, Rₘ。

递归排序:对每个分组Rᵢ(i=1,…,M)递归地应用排序算法。这意味着每个分组都会被读入内存、排序,然后写回磁盘。

合并排序的子表:使用类似于外部归并排序的方法,将所有M个排序后的子表合并成一个大的有序表。这个步骤可能需要多趟读写操作,具体取决于内存大小和关系R的块数。

执行一元操作

如果需要在排序的同时执行一元操作(如去重δ或分组γ),则需要在合并步骤中适当修改算法:

去重(δ):在合并过程中,仅当遇到新的元组时才将其写入输出文件,忽略重复的元组。

分组(γ):首先按分组属性对关系进行排序,然后在合并过程中收集具有相同分组属性值的元组,并根据需要进行聚合操作(如求和、平均等)。

执行二元操作

对于二元操作(如交、连接等),算法也遵循类似的分而治之策略:

分组和排序:对两个关系R和S分别进行分组和排序,生成排序后的子表。

分配缓冲区:根据R和S的块数(B®和B(S))按比例分配M个缓冲区给R和S。

执行操作:使用适当的算法(如嵌套循环连接、归并连接等)来合并排序后的子表,并执行所需的二元操作。这个步骤可能需要多次从磁盘读取和写入数据,具体取决于操作类型和缓冲区大小。

4.8.3 基于散列的多趟算法

** 一元操作**

如果关系R能够装入M个内存缓冲区中(即B® ≤ M),则直接将R读入内存,并在内存中执行所需的一元操作。如果关系R太大,无法装入M个内存缓冲区,则执行以下步骤:

散列到桶中:使用散列函数将关系R的元组散列到M-1个桶中。

递归处理:对每个桶递归地执行一元操作。

合并结果:将所有桶的处理结果合并成一个最终的结果集。

二元操作

如果有一个关系(比如R)能够装入M-1个内存缓冲区中,而另一个关系(比如S)可以逐个块地装入剩下的第M个缓冲区,则可以直接在内存中执行连接操作。具体地,将R读入内存,然后逐个块地将S读入第M个缓冲区,并在内存中执行连接。

如果两个关系都太大,无法同时装入内存,则执行以下步骤:

散列到桶中:将两个关系R和S分别散列到M-1个桶中。对于每个桶i(i=1,…,M-1),R和S都有一个对应的桶R[i]和S[i]。

递归处理:对每个相应的桶对(R[i], S[i])递归地执行连接操作。注意,这里的递归处理与一元操作类似,但操作的是桶对而不是单个关系。

合并结果:将所有桶对的连接结果合并成一个最终的结果集。这个合并过程可能需要额外的排序和归并步骤,以确保最终结果的顺序性(如果需要的话)。

第五章 查询编译器

5.1 语法分析和预处理

5.1.1 语法分析与语法分析树

** 语法分析树的构成**

原子:语法分析树中的叶子节点,对应于源代码中的不可再分的基本元素,如关键字、标识符(如变量名、表名、列名)、常量、标点符号(如括号、逗号)、运算符等。这些元素是词法分析器(Lexer)的输出,词法分析器负责将源代码文本分割成这些基本的词法单元(Tokens)。

语法类:语法分析树中的非叶子节点,代表语法结构中的构造块或“短语”,这些短语由一组原子或更小的语法类通过特定的语法规则组合而成。这些节点用于表示语言中的语法结构,如表达式、语句、程序块等。在表示时,通常会使用尖括号(< >)来标识语法类的名称,以便于区分原子和语法类。

:整个查询的根节点,表示一个完整的SQL查询。

:表示一个SELECT语句,可能包括选择列表、FROM子句、WHERE子句等。

:包含要选择的列名或表达式的列表,如name, age。

:表示对表中某列的引用,如name和age(尽管在这里它们直接作为原子出现,但在更 复杂的表达式中,它们可能是的实例)。

:表示FROM子句,指定查询的数据来源,如FROM users。

:表示表名,如users。

:表示WHERE子句,包含用于过滤结果的条件,如WHERE age > 30。

:表示一个条件表达式,如age > 30。这里可以进一步细分,比如表示比较操作,表示值(尽管在这个例子中30被直接视为原子)。

SELECT name, age FROM users WHERE age > 30;

该查询的语法分析树可能包含以下节点:
<Query>:根节点,表示整个查询。
<SelectList>:包含name和age的列表。
name(原子)
age(原子)
<FromClause>:包含users。
users(原子)
<WhereClause>:包含条件表达式。
<Condition>:表示条件表达式age > 30。
age(原子)
>(原子)
30(原子)

5.1.2 SQL 的一个简单子集的语法

存在一些“基本”或“终端”语法类(有时也称为词法单元或标记),它们不是通过其他语法规则组合而成的,而是直接对应于源代码中的基本元素。这些元素在语法分析阶段之前就已经被词法分析器识别出来。

通常代表数据库表中的列名。在SQL查询中,这些列名用于指定要从表中检索哪些数据。在语法分析树中,的一个实例就是任何当前数据库模式中存在的有效列名。例如,在表employees中,name、age和salary都可能是的实例。

代表数据库中的一个表或视图。在SQL查询中,FROM子句后面通常会跟有一个或多个,以指定查询将要从哪些表中检索数据。同样,在语法分析树中,的一个实例就是任何当前数据库模式中存在的有效表名或视图名。例如,employees、departments和orders都可能是的实例。

在SQL中通常与字符串匹配和搜索相关,尽管在标准的SQL查询中直接使用的情况可能不如和那么普遍。但是,在像LIKE这样的SQL语句中,用于指定要匹配的字符串模式。的一个实例是任何被引号括起来的、符合SQL字符串匹配规则的字符串。例如,在LIKE语句中,'John%'可能是一个的实例,用于匹配以"John"开头的任何字符串。

5.1.3预处理器

** 视图引用的预处理**

当查询语句中引用的关系实际上是一个视图时,预处理器需要将该视图的定义嵌入到原始查询中。

语义检查

确保查询语句在逻辑上是合理的。包括检查关系、属性、类型和运算符的使用是否满足数据库规则。

检查关系的使用:

验证FROM子句中指定的每个关系(表或视图)在当前数据库模式中是存在的。

检查属性的使用:

确保在SELECT子句、WHERE子句等中引用的每个属性都属于当前查询范围内的一个关系。

类型的检查:

验证每个属性的使用是否符合其数据类型的要求。例如,在WHERE子句中使用LIKE运算符时,比较的两边通常应该是字符串或可以隐式转换为字符串的类型。如果使用了不兼容的类型(如将日期类型直接与字符串进行比较),预处理器将需要执行类型转换。

5.2 用于改进查询计划的代数定律

本节列出一些代数定律,用于将一个 表达式树 转换成一个 等价的表达式树 ,后者可能有 更有效的物理查询计划 。应用这些代数变换式的结果是逻辑查询计划,它是查询重写阶段的输出。

数据库系统之:关系代数详解-超详细-CSDN博客

选择(σ):选择运算用于从关系中选择满足特定条件的元组。选择运算的表达式通常表示为σp®,其中p是选择条件,R是关系名。

投影(π):投影运算用于从关系中选择指定的属性列,并删除重复的元组。投影运算的表达式通常表示为πA®,其中A是属性列表,R是关系名。

消除重复(δ:当δ运算符应用于一个关系时,它会检查关系中的每个元组,并移除所有重复的元组。这意味着在结果关系中,每个元组都是唯一的。

笛卡尔积:笛卡尔积是指两个集合(在数据库操作中,可以理解为两个表)的所有可能组合。如果表A有M行,表B有N行,那么A和B的笛卡尔积将有M*N行。每一行都是由表A中的一行与表B中的一行组合而成。不考虑表之间的实际关系,简单地将一个表中的每一行与另一个表中的每一行进行组合。

自然连接:自然连接是一种特殊的等值连接,它自动根据两个表中具有相同名称的列来连接这两个表。如果两个表中存在多个具有相同名称的列,则这些列都会被用作连接条件。连接后,结果表中只保留一个公共列,并去除不满足连接条件的行。根据两个表中具有相同名称的列来连接这两个表,只保留满足连接条件的行,并且结果表中只保留一个公共列。

等值连接(E Inner Join)

等值连接是基于两个或多个表中指定列的值相等进行连接的连接操作。与自然连接不同,等值连接需要显式指定连接条件,即哪些列的值应该相等。连接结果中包含满足连接条件的所有行和列,不会自动去除重复的列。

SELECT * FROM 表A INNER JOIN 表B ON 表A.ID = 表B.ID;

5.2.1 交换律与结合律

** 交换律**

对于某些运算符,其操作数的顺序可以互换而不影响结果。并集(∪)、交集(∩)和笛卡尔积(×)等运算符满足交换律。

并集(∪):对于任意两个关系R和S,如果它们有相同的结构(相同的列名),则R ∪ S = S ∪ R。

交集(∩):对于任意两个结构相同的关系R和S,R ∩ S = S ∩ R。

笛卡尔积(×):虽然笛卡尔积通常用于不同结构的关系之间,但如果我们考虑两个关系R和S(不必有相同的结构),则R × S和S × R的结果在逻辑上是不同的(因为结果关系中的元组顺序和属性排列会不同)。然而,在关系代数的上下文中,我们通常只关注元组的内容而不关注其顺序,因此可以认为笛卡尔积在“无序”的意义上满足交换律。

结合律

并集(∪)、交集(∩)和笛卡尔积(×)等运算符也满足结合律。

并集(∪):对于任意三个结构相同的关系R、S和T,(R ∪ S) ∪ T = R ∪ (S ∪ T)。

交集(∩):(R ∩ S) ∩ T = R ∩ (S ∩ T)。

笛卡尔积(×):对于任意两个关系R和S,以及另一个关系T(T的结构可以与R和S不同),(R × S) × T 和 R × (S × T) 在逻辑上是不等价的(因为结果关系的结构和元组数会不同)。但是,如果我们考虑将笛卡尔积的结果视为一个单一的关系,并仅关注元组的内容(而不考虑其内部结构),则可以认为笛卡尔积满足结合律。

5.2.2 涉及选择的定律

"下推选择"(Pushing Selections Down)是一种重要的优化策略,旨在通过重新排列查询计划中的操作顺序来减少处理的数据量,从而提高查询效率。

下推选择是指在构建查询执行计划时,将选择(或称为过滤)操作尽可能地移动到查询计划的早期阶段,即靠近数据源(如表扫描或索引扫描)的位置。这样做的目的是减少后续操作需要处理的数据量,从而提高查询的整体性能。

1.对于并,选择必须下推到两个参数中。

2.对于差,选择必须下推到第一个参数,下推到第二个参数是可选的。

3.对于其他运算符,只要求选择下推到其中一个参数。对于连接和积,将选择下推到两个参数是没有意义的,因为参数可能有也可能没有选择所要求的属性。即使可以同时下推到两者该做法也不一定能改进计划。选择条件可能依赖于两个表之间的交互数据,而这些数据在连接或积操作之前是不可见的。

假设关系R具有C中要求的全部属性:

5.2.3 下推选择

当查询包含虚视图时,有时首先将选择尽可能往树的上部移是很必要的,然后再把选择下推到所有可能的分支。

选择前置:原始查询中的选择条件(即电影年份为1996年)实际上可以前置到连接操作之前。我们可以先对Movies表进行过滤,只选择1996年的电影,然后再与StarsIn表进行连接。

选择下推:在将选择前置后,我们可以将这个选择条件进一步下推到连接的子节点上。因为year是Movies表的属性,也是连接操作需要考虑的属性,所以我们可以将这个条件分别应用于Movies表和StarsIn表。连接操作只需要处理与1996年电影相关的StarsIn记录,从而显著减少了处理的数据量。

5.2.4 涉及投影的定律

** 投影的下推**

当投影操作可以下推到其他操作(如连接、排序等)中时,可以优化查询的执行计划。投影操作只是减少了每个元组的长度,对总处理量的减少作用有限。因此,在选择和投影都能下推的情况下,优先选择下推选择操作往往更为有效。

扩展投影

扩展投影是投影操作的一个扩展,它允许在投影的过程中不仅选择原始关系中的属性,还可以计算新的属性或表达式作为输出。在扩展投影中,输出属性x可能是一个复杂的表达式,该表达式可以包含输入属性E中的属性、常量或函数运算等。

E->x

输入属性:在投影操作中,E中提到的属性称为输入属性,它们是原始关系中的属性,用于计算或选择输出属性。

输出属性:在投影或扩展投影中,x是输出属性,它可能是原始关系中的一个属性,也可能是通过输入属性计算得到的新值。

简单投影:如果投影列表中的属性只是简单地选择原始关系中的属性,没有包含复杂的表达式或更名操作,则称该投影为简单投影。

5.2.6 有关消除重复的定律

![](https://img-blog.csdnimg.cn/img_convert/0482760ef398b65407cd7d518848e2b3.png)

5.2.7 涉及分组与聚集的定律

** 运算符 γ (Gamma)** : γ 表示聚集运算符,它可以对一组数据进行某种形式的聚合操作,比如求和、计数、平均、最小值或最大值等。

运算符 δ (Delta): 表示去重操作,即从一组数据中删除重复的记录,只保留唯一的记录。

运算符 π (Pi): 是一个选择操作符,用于从一组数据中选择特定的列或属性。

L(属性列表):通常与聚集操作相关联。在聚集函数的表达式中,“L” 指的是聚集操作所应用的属性列表。

M(属性列表):通常用于投影操作。在关系代数中,投影操作(π)用来从关系中选择特定的列或属性。表示从关系 R 中选择属性列表 M 中的所有属性。“M” 在聚集操作中的作用是确保在进行聚集之前,关系中包含所有需要的属性。

聚集运算符的通用定律: δ(γL®) = γL®,意味着先进行聚集操作,然后去重,与先去重再进行聚集操作的结果是相同的。这是因为聚集操作本身不关心数据中的重复项。

投影去除无用属性: γL® = γL(πM®) 表示,在进行聚集操作之前,我们可以选择性地去除那些在聚集列表 L 中没有被使用的属性 M。这是因为这些属性对于最终的聚集结果没有影响。

不受重复影响的聚集: 当聚集列表 L 中只包含 MIN 或/和 MAX 函数时,我们称这个聚集操作是不受重复影响的。这是因为 MIN 和 MAX 函数的结果是不会因为数据中的重复项而改变的。因此,可以有 γL® = γL(δ®),即无论数据是否有重复,最终的聚集结果都是相同的。

受重复影响的聚集: 与不受重复影响的聚集相反,像 SUM、COUNT、AVG 这样的聚集函数,其结果会受到数据中重复项的影响。如果在计算这些聚集之前去除重复项,最终的值可能会不同。

5.3 从语法分析树到逻辑查询计划

** 第一步:使用关系代数运算符替换语法树**

关系代数是一组抽象的操作,用于对关系(即表)进行查询和修改。这些操作包括选择(σ)、投影(π)、连接(⨝)、并(∪)、差(-)、笛卡尔积(×)等。

表引用:直接映射为关系代数中的关系(即表)。

选择条件:转换为选择(σ)操作,用于过滤满足条件的元组。

投影:将SELECT子句中的字段名转换为投影(π)操作,用于选择特定的属性。

连接:根据JOIN条件(如A.dept_id = B.dept_id),转换为连接(⨝)操作,合并两个或多个关系。

第二步:优化关系代数表达式

在得到逻辑查询计划(即关系代数表达式)后,下一步是优化它以生成更有效的物理查询计划。

查询重写:通过等价变换(如改变连接顺序、合并选择操作等)来减少计算量。

索引利用:识别并利用索引来加速查询执行。

并行处理:如果可能,将查询分解为可以并行执行的部分。

5.3.1 转换成关系代数

您“简单”SELECT-FROM-WHERE结构转换为关系代数表达式的规则。

处理FROM列表:

将FROM列表中提及的所有关系(即表)视为关系代数中的基本关系。如果FROM列表中包含了多个表,并且这些表之间需要通过某种方式(如JOIN条件)相关联,则首先计算这些表的笛卡尔积。

处理WHERE条件:

使用选择(σ)运算符来过滤满足WHERE子句中条件的元组。将WHERE子句中的条件表达式转换为关系代数中的选择条件。

处理SELECT列表:

使用投影(π)运算符来选择SELECT列表中指定的属性。投影运算符作用于经过选择和连接(如果有的话)处理后的关系上。

5.3.2 从条件中去除子查询

![](https://img-blog.csdnimg.cn/img_convert/08b90852e1cff82f63407ad548afc943.png)

5.3.3 逻辑查询计划的改进

** 选择条件下推**

选择条件下推是一种将选择(过滤)操作尽可能地向查询计划树的底部(即数据源)移动的优化策略。当查询中包含多个连接的表时,将选择条件尽早应用于较小的数据集可以减少后续操作的数据量,从而提高查询效率。对于AND连接的多个条件,可以分别将每个条件下推到树的相应位置。

投影下推

投影下推是另一种优化策略,它将投影(即选择列)操作尽可能地向查询计划树的底部移动。这有助于减少在数据传输和处理过程中所需的数据量。

重复消除

重复消除是指移除查询结果中的重复行。在某些情况下,这个操作可以被移动到查询计划树中更方便的位置,以减少需要处理的数据量。

选择与积的结合

在某些情况下,可以将选择条件与下面的连接操作结合,将等值条件的选择操作转换为等值连接。这通常可以提高查询的效率,因为等值连接通常可以更有效地利用索引和连接算法。

5.4 运算代价的估计

在将逻辑查询计划转换为物理查询计划的过程中,有四个方面值得关注。

1. 满足结合律与分配律的运算的次序与分组

结合律:对于满足结合律的运算(如连接、并、交),其操作对象的次序可以改变而不影响最终结果。在物理计划中,我们可以重新排列这些运算的次序,以最小化数据移动、减少中间结果的大小或利用系统的并行处理能力。

分配律:在某些情况下,我们可以利用类似分配律的性质来优化查询。例如,将选择(过滤)操作“分配”到连接操作之前,以减少需要连接的数据量。

分组:将多个可结合的运算组合成一个更大的运算单元,可以减少运算符之间的数据交换次数,提高执行效率。例如,将多个连接操作组合成一个复合连接操作

2. 逻辑计划中每个运算符的算法选择

在逻辑计划中,我们定义了要执行的运算,但在物理计划中,我们需要选择实现这些运算的具体算法。例如,对于连接操作,我们可以选择嵌套循环连接、散列连接或排序合并连接等算法。

3. 物理计划中的其他运算符

扫描:物理计划需要明确如何从存储介质(如磁盘)中读取数据。这通常涉及扫描操作,可以是全表扫描或索引扫描。

排序:在物理计划中,排序操作可能是显式的(如ORDER BY子句)或隐式的(如连接操作前的排序)。排序算法的选择和数据的物理布局都会影响排序操作的效率。

4. 参数和数据的传输方式

在物理计划中,我们需要考虑数据如何在不同的运算符之间传输。这包括确定中间结果的存储位置(如内存、磁盘)以及传输机制(如迭代器、批量传输)。使用磁盘上的中间结果可以减少内存的使用,但会增加I/O成本。使用迭代器可以逐条处理数据,减少内存占用,但可能增加CPU的开销。

5.4.1 中间关系大小的估计

主要关注查询执行过程中产生的临时结果(即中间关系)的元组数或数据量。这是评估查询性能的一个重要方面,因为中间关系的大小直接影响了查询所需的存储空间、I/O操作次数以及后续运算的复杂度。

5.4.2 投影运算大小的估计

** 对于关系 R:**

每个元组大小:12(元组头)+ 4(a)+ 4(b)+ 100(c)= 120字节。

块中元组数:(1024−24)/120=8(向下取整)。

元组总数 T®=10000,因此块数 B®=10000/8=1250。

对于关系 S=π + ®(其中 + 表示扩展投影,用 a 与 b 的和替代 a 和 b):

新的元组结构包括:12(元组头)+ 4(a+b的和,假设结果仍为整数)+ 100(c)= 116字节。

尽管元组大小略有减少,但由于仍然可以存放8个元组在一个块中,所以块数和元组总数保持不变:T(S)=10000,B(S)=1250。

对于关系 U=π a,b ®(即传统的去除 c 的投影):

新的元组结构包括:12(元组头)+ 4(a)+ 4(b)= 20字节。

由于元组大小显著减小,现在每个块可以存放更多的元组:(1024−24)/20=50(向下取整)。

因此,虽然元组总数仍然是 T(U)=10000,但块数大大减少:B(U)=10000/50=200。

5.4.3 选择运算大小的估计

** 等值比较:**

当查询条件为某个属性等于某个常量时(如 S = σA=c®),那么结果集的大小可以通过 T(S) = T® / V(R, A) 来估计,其中 T® 是表 R 的元组数,V(R, A) 是属性 A 的不同取值的数量。这个假设在属性值均匀分布时较为准确。

非等值比较:

对于非等值比较(如 S = σA<c® 或 S = σA>c®),由于条件的非确定性,结果集的估计更加复杂。一种假设是认为满足条件的元组大约占总元组数的一半,然而涉及非等值比较的查询倾向产生更小量的元组因此我们估计: T(S) = T® / 3。

特殊比较:

对于某些特殊的选择条件,如检查某个属性是否非空(A IS NOT NULL),如果大多数元组都满足这个条件(即空值较少),则可以假设所有元组都满足条件,即 T(S) = T®。或者,更精确地,可以通过 T(S) = T® * (V(R, A) - 1) / V(R, A) 来估计。

5.4.4 连接运算大小的估计

** 值集的包含:**

如果一个属性Y同时出现在关系R和S中,且R的Y值集合是S的Y值集合的子集(即V(R,Y) ≤ V(S,Y)),那么R中的每个Y值都会在S中出现。这个假设在Y是S的键且是R的外键时成立,但在其他情况下可能不成立。

值集的保持:

在进行连接时,非连接属性(即只出现在一个关系中的属性)不会丢失其值集中的任何值。这意味着,如果A是R的属性但不是S的属性,那么V(R∞S, A) = V(R, A)。这个假设在大多数情况下是合理的,但也可能在某些特殊情况下不成立。

概率计算与结果集大小估计

如果V(R,Y) > V(S,Y),那么S中每个Y值都会出现在R中,因此s的Y值出现在R中的概率为1,但r和s具有相同Y值的概率为1/V(R,Y)(因为R中有V(R,Y)个可能的Y值)。

如果V(R,Y) < V(S,Y),则情况相反,r的Y值会出现在S中,r和s具有相同Y值的概率为1/V(S,Y)。

为了统一这两种情况,我们可以将概率估算为1/max(V(R,Y), V(S,Y))。

基于上述概率,我们可以估算连接结果集T(R∞S)的大小:T(R∞S)= T®×T(S)/max( V(R,Y),V(S,Y) )

5.4.5 多连接属性的自然连接

当连接R(X,Y)xS(Y,Z)中的属性集Y包含多于一个属性时

R⋈S的大小估计是通过T®乘以T(S),对于每一个R与S的公共属性y,除以V(R,y)与V(S,y)中较大者来计算。

假设我们有两个关系R(X, Y)和S(Y, Z),其中Y是两个关系的共同属性集,并且Y包含多个属性(y1, y2, …, yn)。我们可以尝试使用以下方法来近似计算自然连接结果集的大小:

5.4.6 多个关系的连接

在考虑多个关系(表)的自然连接时,特别是当某个属性A出现在多个关系中时,我们需要分析这些关系在连接后如何影响结果集的大小以及属性A上值的分布。以下是对您提出的问题的详细解答:

假设有 k 个关系 R1,R2,…,R__k,它们通过自然连接合并成一个新的关系 S,即 S=R1⋈R2⋈…⋈R__k。属性 A 出现在这 k 个关系中的至少两个关系中。

属性A上值的分布

假设属性 Ak 个关系中的值集大小分别为 u1,u2,…,uk,且已按从小到大的顺序排列,即 u1≤u2≤…≤uk

值集保持假设:在连接后的关系 S 中,属性 A 的值集大小将是这些 ui 中的最小值,即 u1。

所选元组在属性A上相同的概率

假设首先从具有最小 u1 的关系 R1 中选择一个元组 t1,其属性 A 的值为 a

对于其他关系 Ri(其中 i=2,3,…,k),所选元组 ti 在属性 A 上与 t1 相同的概率是 ui1(因为 Ri 中有 ui 个不同的 A 值)。

因此,所有 k 个元组在属性 A 上相同的概率是这些概率的乘积,即 1/u1u2…uk

估计连接后的大小

一个近似的估计方法是:对于每个这样的属性 A,从乘积中除以除了 V(Ri,A) 中的最小值(即 u1)之外的所有 V(Ri,A) 的较大值的乘积

5.4.7 其他运算大小的估计

** 并**

包的并:结果的大小正好是参数关系大小之和。

集合的并:结果的大小介于两参数大小之和到两参数中较大者之间。建议取中间值,如较大者加上较小者的一半。

结果的大小可以从0个元组(无交集时)到两参数中较小者(完全重叠时)之间变化。建议取两极端的平均值,即较小值的一半。

当计算 RS 时,结果中的元组数可以从 T(R)(S 为空时)到 T(R)−T(S)(S 完全包含在 R 中时)之间变化。建议估计值取其平均值:T(R)−T(S)/2。

消除重复

如果 R(a1,a2,…,an) 是一个关系,则去重后 δ(R) 的大小理论上等于 R 中不同元组的数量。由于这个统计值通常不可得,需要取近似值。极端情况下,去重后的大小可以是 T(R)(无重复元组时)或1(所有元组都相同时)。另一个上限是可能存在的不同元组的最大数,即所有 V(R,ai)(i=1,2,…,n)的乘积。然而,这个数可能远大于 T(R) 的实际值。一个合理的估计是取 T(R)/2 与所有 V(R,ai) 之积中的较小者。

分组(GROUP BY)与聚集

对于分组操作 γL(R)(其中 L 是分组属性列表),结果中的元组数与分组数相同。

分组数的范围可以从1(所有元组在分组属性上都相同)到 T(R)(每个元组在分组属性上都是唯一的)。

与去重类似,可以使用所有 V(R,A)(其中 AL 中的属性)的乘积作为分组数的上界。建议的估计值是 T(R)/2 与这个乘积中的较小者。

5.5 基于代价的计划选择介绍

5.5.1 大小参数估计值的获取

** 元组数和不同值数目的获取**

元组数(T®):通过对整个关系R进行一次扫描,可以简单地计算出关系中的元组数。

不同值数目(V(R,A)):通过扫描关系R并跟踪每个属性A的不同值,可以计算出该属性列的不同值数目。

块数(B®)的估计 : 如果关系R是聚簇存储的,那么它所占用的块数(B®)可以通过实际使用的块数来计数。如果关系不是聚簇存储的,或者需要更粗略的估计,则可以通过将元组数(T®)除以一个磁盘块可以容纳的R的元组个数来估算块数。

直方图的计算

等宽直方图:将属性值的范围划分为等宽的区间,并计算每个区间内的元组数。如果遇到新的更小的值,可能需要调整区间的边界。

等高直方图:基于百分比来划分区间,例如列出最小值、比最小值多p%的值、比最小值多2p%的值等,直到最大值。

最频值直方图:列出最常见的值及其出现次数,同时可能包含其他值的分组统计。

基于直方图的连接大小估计

使用直方图可以更准确地估计连接操作的结果大小。特别是当连接属性的值显式地出现在两个关系的直方图上时,可以精确地知道结果中有多少元组将具有该值。

5.5.2 统计量的计算

** 周期性更新的原因**

稳定性:数据库中的统计量在短时间内通常不会发生剧烈变化,因此没有必要频繁更新。

一致性:即使统计量不是绝对精确的,只要它们被一致地应用于所有查询计划,优化器仍然能够进行有效的比较和选择。

性能考量:频繁更新统计量会将统计量本身变成数据库中的“热点”,因为它们会被频繁读取和写入,这会影响数据库的整体性能。

取样大小:取样的元组数量需要根据统计精度和计算成本之间的平衡来确定。例如,可以选择关系R的1%作为样本。

5.5.4 枚举物理计划的方法

选择最优的物理查询计划:

穷尽法:穷尽法是一种最直接但可能最耗时的方法,它尝试所有可能的物理计划组合,并对每个计划进行代价估计,最终选择代价最小的计划。这种方法适用于小型数据库或查询,但对于大型数据库和复杂查询来说,其计算量将变得不可接受。

自顶向下与自底向上方法

自顶向下(Top-Down):从逻辑查询计划树的根部开始,逐级向下考虑每个运算符的可能实现,并计算每种组合的代价。这种方法在每一步都需要评估所有可能的子计划,并可能导致大量的重复计算。

自底向上(Bottom-Up):从逻辑查询树的叶子节点(即基本关系)开始,逐级向上计算每个子表达式的最优计划,并在构建较大子表达式的计划时只考虑较小子表达式的最优计划。这种方法通常与动态规划相结合,以减少重复计算并提高效率。

动态规划:动态规划是自底向上方法的一个变种,它通过保存子问题的解来避免重复计算。对于每个子表达式,动态规划方法只保留其最小代价的计划,并在构建更大子表达式的计划时利用这些已保存的最优解。这种方法虽然不保证总是找到全局最优解,但在许多情况下都能获得很好的结果。

Selinger 风格的优化:是对动态规划方法的一种改进。它不仅记录了每个子表达式的最小代价计划,还记录了那些具有较高代价但结果顺序对上层运算符有用的计划。这种方法利用了查询计划中的某些特性(如排序、分组和连接属性),以期望从非最优的子表达式计划中构建出整体最优的查询计划。

启发式选择:基于一系列启发式规则来选择物理计划。这些规则通常基于经验或统计信息,旨在快速找到近似最优的查询计划。例如,如果连接的一个参数在连接属性上有索引,则采用索引连接;如果需要对多个关系进行并或交操作,则先对最小关系进行组合等。启发式选择方法通常比穷尽法更快,但可能无法找到全局最优解。

分支界定计划枚举:通过启发式方法找到一个好的初始物理计划,并以此为基准来剪枝搜索空间。在搜索过程中,任何代价高于当前已知最优计划的计划都将被放弃,因为它们不可能成为更优的完整查询计划的一部分。这种方法可以显著减少搜索空间的大小,并提高搜索效率。

爬山法:从一个初始的物理计划开始,通过不断地对计划进行小的修改(如替换运算符的实现方法、重新排序连接等)来寻找代价更低的邻近计划。当无法再通过小修改来降低计划的代价时,算法停止并返回当前的计划作为最优解。这种方法可能陷入局部最优解,但通常能够快速找到一个可接受的解决方案。

5.6连接顺序的选择

5.6.1 连接的左右参数的意义

** 一趟连接:** 通常选择较小的关系作为左参数(构造用关系),并将其加载到内存中,构建成一个哈希表。这个哈希表使得数据查找变得非常高效。然后,将较大的关系(右参数,探查用关系)分批读入内存,并将每个元组与哈希表中的元组进行匹配,从而执行连接操作。这种策略利用了较小的关系能够完全加载到内存中并构建高效索引的优势。

嵌套循环连接:左参数通常被选作外部循环关系。这意味着查询优化器会遍历左参数中的每一个元组,并将其与右参数中的所有元组进行比较,以查找匹配项。如果左参数较小,则外部循环的开销相对较小,这有助于提升整体性能。如果左参数很大,则可能需要考虑其他连接策略。

索引连接:右参数通常被认为是有索引的关系。这意味着在连接过程中,查询优化器会利用右参数的索引来快速定位匹配的元组,而不是遍历整个关系。因此,选择拥有有效索引的关系作为右参数可以显著提高连接操作的效率。索引连接特别适用于当右参数很大,但可以通过索引快速访问其特定部分时。

5.6.3 左深连接树

![](https://img-blog.csdnimg.cn/img_convert/407ea4bf1a656eeefcfc5ff1e3563bbf.png)

左深树

浓密树

右深树

左深树的数量与所有树的数量

对于给定数目的关系(即树叶),所有可能的树形结构(包括非左深树和左深树)的数量是巨大的,特别是随着关系数量的增加,这个数量呈指数级增长。相比之下,左深树作为这些所有可能树形结构的一个子集,其数量虽然也很大,但增长速度要慢得多。这是因为左深树的结构限制了树的形状,使得每个节点最多只能有一个右子节点(且该右子节点可以是另一个连接或关系),从而减少了可能的组合方式。

左深树与连接算法的交互

一趟连接:左深树使得优化器能够选择较小的关系作为构建哈希表的关系(即左子树),而将较大的关系或连接结果作为探查的关系(即右子树)。这种安排可以最大化哈希表的效率,因为哈希表可以一次性构建并用于多个连接操作。

嵌套循环连接:在嵌套循环连接中,左深树允许优化器将较小的关系放在外层循环(即左子树),而将较大的关系或连接结果放在内层循环(即右子树)。这样可以减少内层循环的迭代次数,从而提高整体连接的效率。

左深树中的运算符:左深树或右深树中的树叶(即基本关系)不仅可以是简单的关系,还可以是包含其他运算符(如选择、投影)的内部节点。这意味着在构建查询计划时,优化器可以灵活地将这些运算符应用于连接的输入或输出,以进一步减少处理的数据量并优化查询性能。

5.6.4 通过动态规划来选择连接顺序和分组

** 1. 定义状态和问题**

在动态规划中,我们首先定义状态。在这个场景下,状态可以定义为包含一定数量表的集合,以及这些表之间可能的连接顺序和成本。目标是找到包含所有目标表的一个集合的最优连接顺序,使得总成本最小。

2. 构建代价表

代价表(或称为DP表)用于存储中间结果,即对于每个可能的表集合,其最优连接顺序及其对应的成本。这个表通常通过归纳法来填充。

基础情况:对于只包含一个表的集合,其最优连接顺序就是该表本身,成本为0(因为没有进行连接)。

归纳步骤:对于包含多个表的集合,我们考虑所有可能的子集划分,并计算每种划分下的连接成本。我们选择成本最小的划分作为该集合的最优连接顺序。

3. 考虑左深树与其他树形

左深树:在只考虑左深树的情况下,连接顺序的选择相对简单。对于集合R中的k个表,我们尝试将R划分为R-i和{i}(其中i是R中的一个表),并计算R-i与i连接的成本。我们选择成本最小的划分作为最优解。

所有可能的树形:如果考虑所有可能的树形,问题变得更加复杂。我们需要考虑所有可能的子集划分,并计算每种划分下将子集连接起来的成本。这通常涉及更复杂的成本函数和更多的计算。

4. 计算成本和表达式

对于每种划分,我们需要计算两个主要值:

连接成本:这是将两个子集连接起来的成本,通常基于它们的大小、索引使用情况、以及可能的公共属性等因素。

结果大小:这是连接操作后结果集的大小,用于后续连接的成本计算。

5. 表达式构建

在找到最优连接顺序的同时,我们还需要构建相应的表达式,这个表达式描述了表之间的连接顺序。在左深树的情况下,表达式就是表的顺序;在更一般的情况下,表达式可能包括嵌套的连接操作。

6. 应用到实际查询

最终,我们得到的代价表和相应的表达式可以被用来优化实际的数据库查询。通过选择成本最低的连接顺序,数据库管理系统可以减少查询的执行时间,提高查询效率。

5.6.5 带有更具体的代价函数的动态规划

仅基于关系大小来估计连接成本可能会忽略一些重要的实现细节,如索引的使用、磁盘I/O成本等。为了更准确地估计连接成本,我们可以对动态规划算法进行修改,以纳入更具体的代价函数,如磁盘I/O成本。在动态规划算法中,我们可以修改代价的计算方式,以考虑实际的连接成本。这通常涉及以下几个方面:

磁盘I/O成本:对于每个关系,我们需要知道其数据在磁盘上的存储方式(如是否索引),以及访问这些数据所需的磁盘I/O次数。

连接算法:不同的连接算法(如嵌套循环连接、排序合并连接、散列连接)具有不同的成本特性。我们需要根据关系的特性和可用索引来选择最合适的连接算法。

中间结果的大小:连接操作的结果大小也会影响后续操作的成本。较大的中间结果可能需要更多的磁盘空间,并增加后续操作的I/O成本。

Selinger风格优化:是一种更全面的查询优化方法,它不仅考虑连接操作的成本,还考虑查询结果的排序和存储顺序。这种方法对于生成高效的查询计划特别有用,特别是当查询结果需要按照特定顺序返回给用户时。在Selinger风格优化中,对于每个可能被连接的关系集合,我们不仅计算连接的最小成本,还考虑生成以几个“感兴趣”的顺序中的任意一个顺序存储的关系的最小成本。这些“感兴趣”的顺序可能包括:

对后续排序连接有利的顺序:如果查询计划中包含排序连接,那么生成已经排序的中间结果可能会减少后续操作的成本。

用户期望的输出顺序:如果查询结果需要按照特定顺序返回给用户,那么生成符合这一顺序的中间结果可能会减少最终排序的成本。

5.6.6 选择连接顺序的贪婪算法

在数据库查询优化中,特别是在处理多表连接(JOINs)时,选择最优的连接顺序对于提高查询效率至关重要。当连接的表数量增多时,可能的连接顺序组合呈指数级增长,这使得穷尽搜索所有可能的连接顺序变得不现实。因此,启发式算法如贪婪算法成为了一个实用的选择。

贪婪算法在连接顺序选择中的应用

贪婪算法在选择连接顺序时,遵循局部最优原则,即每一步都选择当前看来最好的选择,从而希望导致全局最优解。

初始化:选择一个或多个基础表(通常是较小的表或者索引良好的表)作为起始点。初始化一个空的连接树,这个树开始时只包含选定的基础表。

迭代选择:在所有尚未被添加到当前连接树中的表中,选择一个表与当前连接树的根(或某个节点)进行连接,这个选择基于某种估计的成本或大小(如基于统计信息的行数估计)。将选中的表添加到连接树中,形成新的连接树结构。

5.7 物理查询计划选择的完成

** 1. 算法选择**

在逻辑查询计划转换为物理查询计划的过程中,需要为各个操作选择合适的算法。

2. 中间结果的物化和流水操作

物化:物化是指将查询的中间结果完全存储在磁盘上。这种方式在内存不足或需要重用中间结果时非常有用。然而,物化会增加磁盘IO操作,降低查询效率。

流水操作:流水操作是指在执行查询时,中间结果只在内存中临时存储,不保存到磁盘上。这种方式可以减少磁盘IO,但要求系统有足够的内存来支持所有中间结果的存储。

5.7.1 选取一个选择方法

分析不同算法的代价

1. 表扫描算法与过滤器步骤结合

代价估计:如果R被聚集(Clustered):B®,其中B®是关系R的数据块数量。聚集意味着数据在物理存储上按某个特定属性(通常是主键或索引键)的顺序排列,但这对于表扫描的代价估计通常不直接影响,因为表扫描仍需检查所有块。

如果R没有被聚集:T®,这里T®可能表示关系R中所有元组的总数,但在I/O代价的上下文中,更准确地应该是T®除以每块能容纳的元组数,但通常简化为B®,即数据块的数量,因为I/O操作以块为单位进行。

2. 利用等值索引扫描

代价估计:如果索引是聚集的:B®/V(R,a),其中V(R,a)是属性a在关系R中的唯一值数量(或近似值)。聚集索引意味着数据本身就是按照索引的顺序存储的,因此可以更快地定位到满足条件的元组。但代价估计中仍然考虑了整个关系的数据块数量,因为即使索引是聚集的,也可能需要遍历多个块来找到所有匹配的元组。

如果索引不是聚集的:T®/V(R,a)。非聚集索引不包含数据本身,只包含指向数据块的指针,因此需要额外的I/O来检索实际的数据行。但在这个简化的估计中,我们假设通过索引可以快速定位到包含所需数据的块。

3. 利用不等值索引扫描

如果索引是聚集的:B®/3.9。这个估计试图反映不等值查询可能需要检查比等值查询更多的块,因为不等值条件可能匹配多个连续的数据块。然而,这个数字3.9是一个高度简化的假设,实际中可能需要更复杂的模型来准确估计。

如果索引不是聚集的:T®/3。对于非聚集索引,不等值查询同样需要额外的I/O来检索数据行,但代价估计的具体数字取决于索引和数据的实际情况。

5.7.3 /4流水操作与物化

流水操作:流水操作是一种更有效的方法,它允许同时交错进行多个运算,减少了磁盘I/O的需求。在流水操作中,一个运算产生的结果元组直接传递给需要它的运算,而不需要将中间结果存储在磁盘上。这种方法通常由迭代器网络执行,迭代器网络中的迭代器在适当的时候互相调用,实现数据的流动。

一元流水:一元流水操作是指涉及单个输入源的操作,如选择(Selection)和投影(Projection)。在这种操作中,消费者(即需要结果的查询部分)通过调用迭代器的<font style="color:#000000;">GetNext()</font>方法来请求下一个元组。

二元流水:二元运算涉及两个输入参数,如连接(Join)或合并(Union)操作。二元运算的结果也可以进行流水操作。我们使用一个缓冲区将结果传递给消费者,一次一块。然而,计算结果和消费结果所需的其他缓冲区数目是不同的,它们取决于结果的大小以及参数的大小。我们将使用一个扩展的例子来演示折中和机会。

5.7.7 物理运算的排序

** 物理查询计划树的分解与执行**

树的分解:当查询计划以树的形式表示时,我们可以通过物化(即存储中间结果)来分解这棵树。物化意味着在树中的某些节点处,将中间结果存储在磁盘上,以便后续运算使用。这样做可以将复杂的查询计划分解成一系列较小的、更易于管理的子树。

子树的执行顺序:在物化策略下,子树的执行顺序通常是按照从下到上、从左到右的前序遍历顺序进行的。这种顺序确保了每个子树在其依赖的子树完成执行后才能开始执行,从而保证了数据的正确性和完整性。

迭代器网络与流水操作

对于采用流水操作(也称为流水线操作)的物理查询计划,迭代器网络是实现这一策略的关键。迭代器网络由一系列相互连接的迭代器组成,每个迭代器代表查询计划中的一个运算。迭代器之间通过调用GetNext等方法来传递数据,从而实现了数据的直接传递和连续处理,而无需将中间结果存储在磁盘上。

迭代器网络中的事件顺序

在迭代器网络中,事件的顺序是由各个迭代器之间的交互和调用关系决定的。具体来说,当一个迭代器需要数据时,它会调用其上游迭代器的GetNext方法。这个调用会触发上游迭代器执行必要的运算,并将结果返回给下游迭代器。通过这种方式,整个查询计划中的运算被同步地执行,而事件的确切顺序则是由这些调用关系决定的。

查询优化与执行代码生成

基于上述策略,查询优化器可以为给定的查询生成相应的执行代码。这些代码通常是一系列函数调用的序列,它们按照预定的顺序执行查询计划中的各个运算。通过这种方式,数据库系统能够高效地处理查询请求,并返回准确的结果。

第6章系统故障对策

** 日志** :是支持数据可恢复性的基础技术。日志以安全的方式记录了数据库中所有变更的历史,包括数据的增、删、改操作。

日志类型

  • Undo 日志:记录了如何将数据库从当前状态回滚到某个之前的状态。主要用于处理事务的撤销操作,确保在事务失败或需要回滚时,数据库能够恢复到事务开始前的状态。
  • Redo 日志:记录了如何将数据库从某个旧状态重新应用更改以恢复到当前状态。在系统故障后,可以使用这些日志来重做所有未完成的更改,以恢复数据库的最新状态。
  • Undo/Redo 日志:结合了上述两种日志的特性,既能够回滚也能够重做数据库的操作,提供了更灵活的恢复策略。

Undo日志和Redo日志在数据库系统中各有其独特的作用和应用场景。Undo日志主要用于事务的回滚操作,确保事务的原子性和一致性;而Redo日志则主要用于系统的恢复和故障处理过程,确保数据库在崩溃或故障后能够恢复到一致的状态。两者共同协作,为数据库系统的可靠性和稳定性提供了重要保障。

检查点技术:减少恢复过程中需要检查的日志量,提高恢复效率。

工作原理:定期在数据库系统中创建一个检查点,该点表示此时刻数据库的状态是一致的。在检查点时刻,系统会记录当前所有事务的状态(如已提交、未提交等)和日志信息的位置。如果系统发生故障,恢复过程只需要从最近的检查点开始,而不是从头开始检查日志,从而大大减少了恢复所需的时间和资源。

6.1 可恢复操作的问题和模型

6.1.1 故障模式

** 1. 错误数据输入**

:::danger
错误数据输入可能由人为因素(如键盘输入错误)或系统错误引起。这些错误可能难以被自动检测,特别是当错误数据在格式上仍然符合规则时(如电话号码中的一位数字错误)。

:::

:::color2
应对措施

编写约束:通过数据库管理系统提供的约束功能(如NOT NULL、UNIQUE、CHECK等),限制输入数据的类型和范围,确保数据在逻辑上的一致性。

触发器:使用触发器在数据被插入、更新或删除时自动执行特定的检查或操作,以识别并处理潜在的数据错误。

:::

2. 介质故障

:::danger
介质故障包括磁盘的局部故障(如单个扇区损坏)和全局故障(如磁头损坏导致整个磁盘不可访问)。

:::

:::color2
应对措施:

奇偶校验:利用与磁盘扇区相关联的奇偶校验来检测并纠正局部故障。

RAID技术:通过配置RAID(独立磁盘冗余阵列)来提高数据的可用性和容错性。RAID可以通过数据条带化、镜像、校验等方式来减少单个磁盘故障对数据完整性的影响。

备份:定期创建数据库的完整或增量备份,并将备份存储在远离主数据库的安全位置。这样,在发生介质故障时,可以通过恢复备份来恢复数据。

分布式冗余拷贝:在多个物理节点上保存数据库的冗余拷贝,以提高系统的可靠性和容错性。同时,需要维护这些拷贝之间的一致性,确保数据的准确性。

:::

3. 灾难性故障

:::danger
灾难性故障包括数据库所在物理环境的完全毁坏,如火灾、爆炸或恶意破坏,导致所有数据介质同时失去作用。

:::

:::color2
备份和冗余:与介质故障相似,但备份需要更加频繁和全面,以确保在灾难发生后能够恢复尽可能多的数据。

分布式冗余拷贝:将数据库的冗余拷贝分布在不同地理位置的多个节点上,以减少单一地点灾难对系统的影响。

:::

4. 系统故障

:::danger
系统故障通常指的是导致事务状态丢失的问题,如掉电或软件错误。由于内存是易失性的,掉电会导致主存中的数据丢失,包括事务的当前状态和修改结果。

:::

:::color2
日志记录:使用非易失性的日志来记录所有对数据库的更新操作。这样,在系统故障后,可以通过日志来恢复事务的状态和数据库的一致性。

恢复机制:开发复杂的恢复机制,确保日志记录能够在不受故障干扰的情况下进行。这些机制通常包括日志的持久化、事务的原子性保证以及恢复算法等。

:::

6.1.2 关于事务的进一步讨论

事务是执行数据库操作的基本单位,它确保了数据的一致性和完整性。

事务的四个基本特性(ACID特性):

原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不执行,不能存在部分执行的情况。

一致性(Consistency):事务执行的结果必须使数据库从一个一致性状态转变到另一个一致性状态。

隔离性(Isolation):并发执行的事务之间互不干扰,每个事务都独立运行,如同没有其他事务在并发执行一样。

持久性(Durability):一旦事务被提交,它对数据库的修改就是永久性的,即使系统发生故障也不会丢失。

日志管理器负责维护日志,确保事务的日志记录能够正确地存储在磁盘上。由于日志记录最初是存储在内存中的,因此它们需要在适当的时候被拷贝到磁盘上,以确保数据的持久性。日志管理器与缓冲区管理器交互,以管理日志记录的存储和检索。

恢复管理器在数据库系统崩溃时被激活。它检查日志记录,并使用这些记录来恢复数据库到一致的状态。恢复管理器可能需要回滚未提交的事务,或者重做已提交但尚未写入磁盘的事务,以确保数据库的完整性和一致性。

6.1.3 事务的正确执行

** 数据库元素与状态**

数据库元素(如关系、磁盘块/页、元组等)是数据库操作的基本单位。选择磁盘块或页作为数据库元素的原因在于,它们通常是数据库管理系统(DBMS)中数据读写操作的最小单位。这样做可以简化事务日志和恢复机制的设计,因为每次事务操作都可以针对完整的磁盘块进行记录,从而避免了部分数据写入导致的复杂问题。

数据库状态的一致性

数据库状态的一致性不仅要求满足数据库模式中的显式约束(如键约束、值约束等),还需要满足隐式约束。隐式约束可能来源于业务逻辑、数据完整性规则或用户界面的限制等。确保数据库状态的一致性对于维护数据的完整性和可靠性至关重要。

事务的正确性原则

如果事务在没有其他任何事务和系统错误的情况下执行,并且在它开始执行时数据库处于一致的状态,那么当事务结束时数据库仍然处于一致的状态。

6.1.4 事务的原语操作

** 1. INPUT(X)**

INPUT(X) 操作负责将包含数据库元素 X 的磁盘块读取到主存中的一个缓冲区中。

2. READ(X, t)

READ(X, t) 操作首先检查包含数据库元素 X 的块是否在主存缓冲区中。如果不在,则执行 INPUT(X) 将其加载到缓冲区。然后,将缓冲区中 X 的值赋给事务的局部变量 t。

3. WRITE(X, t)

WRITE(X, t) 操作首先检查包含数据库元素 X 的块是否在主存缓冲区中。如果不在,则执行 INPUT(X)。然后,将事务局部变量 t 的值写入缓冲区中 X 的位置。

4. OUTPUT(X)

OUTPUT(X) 操作负责将包含数据库元素 X 的缓冲区中的块写回到磁盘上。这个操作是确保数据持久性的关键步骤。

6.2 undo 日志

Undo 日志:记录了如何将数据库从当前状态回滚到某个之前的状态。主要用于处理事务的撤销操作,确保在事务失败或需要回滚时,数据库能够恢复到事务开始前的状态。

**6.2.1 日志记录**

日志记录类型的详细解释

1. 记录

含义:这个记录表示事务T已经开始执行。它标记了事务在日志中的起始点,有助于在恢复过程中识别哪些事务可能尚未完成。

作用:在恢复过程中, 记录用于确认事务的存在和开始时间。虽然它本身不直接参与恢复操作,但它是事务日志完整性的重要组成部分。

2. 记录

含义:这个记录表示事务T已成功完成,并且其对数据库的所有修改都应该是永久的。然而由于缓冲区管理器的行为,这些修改可能尚未写入磁盘。

作用:在恢复过程中, 记录是确定哪些事务应该被视为成功完成的依据。然而,由于磁盘写入的不确定性,恢复管理器可能需要额外的步骤来确保所有已提交事务的修改都被正确地写入磁盘。

3. 记录

含义:这个记录表示事务T由于某种原因(如内部错误、死锁、超时等)不能成功完成。

作用:在恢复过程中, 记录用于标识需要回滚(撤销)的事务。恢复管理器将使用undo日志中的更新记录来回滚这些事务所做的所有修改,以确保数据库的一致性。

4. 更新记录 <T, X, v>

含义:这个三元组表示事务T修改了数据库元素X,而X的原始值是v。它记录了事务对数据库的具体修改。

作用:更新记录是undo日志的核心。在恢复过程中,如果事务T被中止(即出现记录),恢复管理器将使用这些更新记录来将数据库元素X的值恢复到它们在事务T开始之前的状态。这样,中止事务对数据库的影响就被消除了。

undo日志的局限性:undo日志仅记录旧值,不记录新值。这意味着它只能用于回滚事务,而不能用于重做已提交事务的修改。

6.2.2 undo 日志规则

** 规则U1** :如果事务T修改了数据库元素X,那么表示这一修改的日志记录(形如

规则U2:如果事务提交,那么它的COMMIT日志记录()必须在事务改变的所有数据库元素都已被写入磁盘之后写入磁盘,但应尽快。这条规则确保了提交操作是持久的,即一旦事务被提交,其修改就不会因为系统故障而丢失。

**6.2.3 使用 undo 日志的恢复**

** 恢复过程**

撤销(Undo)未提交的事务:对于那些在崩溃时尚未提交的事务,恢复管理器需要撤销它们所做的所有修改,以确保这些事务不会留下任何不一致的状态。

重做(Redo)可能丢失的已提交事务的修改:如果某些已提交事务的修改因为系统崩溃而未能完全写入磁盘,恢复管理器需要重做这些修改。

识别事务状态

已提交事务:这些事务的记录已经写入日志。它们的修改被认为是有效的,不需要撤销。

未提交事务:这些事务的记录存在,但记录不存在。它们的修改需要被撤销,以恢复数据库到一致状态。

撤销未提交事务的修改

恢复管理器从日志的末尾开始扫描,向前移动。

当遇到<T, X, v>记录时,如果尚未扫描到记录,则意味着事务T未提交,需要撤销对X的修改。恢复管理器会将X的值改回日志中记录的修改前的值。

如果已经扫描到记录,则不对该事务的修改进行任何操作。

处理事务结束

在完成所有必要的撤销操作后,恢复管理器会为每个未完成的事务T写入记录到日志中,并刷新日志,以确保这些记录被持久化。

6.2.4 检查点

检查点机制通过定期地记录数据库和日志文件的状态,来确保在系统崩溃后能够快速地恢复到最近的一个一致状态。
  1. 停止新事务。
  2. 等待所有活跃事务完成:这包括等待所有事务提交或中止,并在日志中记录COMMIT或ABORT。这一步确保了在检查点时刻,所有未完成的事务状态都是已知的。
  3. 刷新日志到磁盘:将内存中的日志记录强制写入磁盘,确保日志的持久性。
  4. 写入记录:在日志中写入一个特殊的检查点记录,表明此点之后的所有事务都是在此检查点之后开始的。

记录用于标记检查点(Checkpoint)的完成。在记录之后的所有事务都是在当前检查点之后开始的。记录包含了以下关键信息:

时间戳:记录了检查点完成的具体时间点,这对于后续的恢复操作至关重要。

状态信息:可能包含了数据库在检查点时刻的特定状态信息,如活跃事务列表、已提交事务的日志位置等,这些信息有助于恢复过程快速定位到正确的恢复起点。

日志位置:在某些实现中,记录还可能包含当前日志文件的写入位置或检查点相关的日志序列号(LSN),这有助于恢复过程确定从哪些日志记录开始应用或忽略。

通过记录,数据库系统能够在发生崩溃或需要恢复时,快速定位到最近的检查点,并仅从该检查点之后的日志记录中恢复未完成的事务或应用已提交事务的修改。这大大减少了恢复过程所需处理的数据量,提高了恢复效率。

  1. 重新开始接收事务:一旦检查点完成,系统可以继续接收新的事务。

6.2.5 非静止检查点

传统的静止检查点要求在检查点期间停止接收新的事务,等待所有当前活跃的事务完成,并将它们的修改写入磁盘。然而,这种方法会导致系统暂停服务,影响用户体验。

非静止检查点技术允许在系统进行检查点的同时,继续接收和处理新的事务

非静止检查点的步骤

  1. 开始检查点:

系统在日志中写入一个特殊的<STARTCKPT(T,…,T)>记录,其中T,…,T是所有当前活跃事务的标识符。

这个记录表明,从现在开始,系统将开始一个检查点过程,但会继续接收新的事务。

  1. 等待活跃事务完成:

系统继续运行,允许新事务进入并处理。

同时,系统等待记录中列出的所有活跃事务完成(提交或中止),并将它们的修改写入磁盘。

  1. 结束检查点:

当所有活跃事务都完成后,系统在日志中写入一个记录。

这个记录表明,检查点过程已完成,所有在之前开始的事务都已经处理完毕。

恢复过程

当系统从故障中恢复时,它会从日志的尾部开始向后扫描:

如果先遇到记录,那么恢复过程可以安全地忽略之前的所有日志记录,因为它们所代表的事务更新已经稳定地存储在数据库中。恢复过程将继续向后扫描,直到遇到下一个记录。

如果先遇到记录但还没有记录,那么系统崩溃发生在检查点过程中。恢复过程需要找到并撤销那些在和崩溃之间开始且未完成的事务,以及那些在中列出但尚未在崩溃前完成的事务。

6.3 redo 日志

1. undo 日志在恢复时消除未完成事务的影响并忽略已提交事务,而redo日志忽略未完成的事务并重复已提交事务所做的改变。 2. undo日志要求我们在COMMIT日志记录到达磁盘前将修改后的数据库元素写到磁盘,而redo日志要求COMMIT记录在任何修改后的值到达磁盘前出现在磁盘上。 3. 对于undo日志,恢复时需要旧值(即事务开始前的值)来撤销更改。而对于redo日志,恢复时需要的是新值(即事务提交后应该存在于数据库中的值)来重做更改。

6.3.1 redo 日志规则

1. 指出被修改元素的日志记录:

当事务T需要修改数据库元素X时,首先会生成一条形如<T, X, v>的日志记录,其中T是事务标识,X是被修改的数据库元素标识,v是新的值。这条记录会立即被写入到日志文件中,但此时数据库元素X本身还没有被修改。

  1. COMMIT 日志记录:

当事务T完成所有修改并准备提交时,会生成一条的日志记录,表示事务T已经成功完成。这条记录同样会被写入到日志文件中,并且它必须出现在所有与该事务相关的更新记录之后。

  1. 改变的数据库元素自身:

只有当上述所有与事务T相关的日志记录(包括更新记录和提交记录)都成功写入到磁盘上的日志文件中之后,事务T才会实际修改数据库元素X的值,并将这个新的值写入到磁盘上的数据库文件中。

这恢复过程会检查日志文件中的记录,并按照事务的提交顺序重新执行所有已提交的更新操作,从而恢复数据库到崩溃前的状态。

由于先写日志规则的存在,即使数据库元素本身还没有被写入磁盘,但是相关的日志记录已经存在,因此可以通过这些日志记录来恢复数据库的状态。这种机制大大提高了数据库的可靠性和恢复能力。

6.3.2 使用redo 日志的恢复

1. 确定已提交的事务

在恢复过程开始时,首先需要确定哪些事务是已经提交的,哪些事务是未完成的(即已启动但尚未提交或已回滚的事务)。这通常通过检查日志文件中的记录来完成。如果日志中存在记录,则事务T被认为是已提交的;否则,事务T被认为是未完成的。

2. 从首部开始扫描日志

接下来,从日志文件的开始部分顺序扫描每一条记录。对于遇到的每一条<T, X, v>记录(其中T是事务标识,X是数据库元素标识,v是新值),需要执行以下操作:

  • a) 如果T是未提交的事务

在这种情况下,由于事务T最终没有成功提交,因此它对数据库所做的任何修改都不应该被保留。因此,对于这样的记录,恢复过程将忽略它,不执行任何操作。

  • b) 如果T是提交的事务

对于已提交的事务,其修改是有效的,并且需要被应用到数据库中。因此,恢复过程会将新值v写入到数据库元素X中,无论该元素当前的值是什么。这是通过redo日志实现的,即重新执行已提交事务的修改操作。

3. 对每个未完成的事务T,在日志中写入一个记录并刷新日志

在扫描完所有日志记录并应用了所有已提交事务的修改之后,恢复过程需要处理那些未完成的事务。虽然这些事务的修改不会被应用到数据库中,但出于一致性和审计的目的,通常会在日志中为每个未完成的事务写入一个记录。这个记录表明事务T由于某种原因(如系统崩溃)而未能成功完成,并且其修改应该被忽略。为了确保记录被正确地写入到日志文件中,通常需要执行一个“刷新”操作,以确保日志文件的更改被持久化到磁盘上。这样,即使系统再次崩溃,也能够从日志文件中准确地了解哪些事务是已提交的,哪些事务是未完成的。

6. 3. 3 redo 日志的检查点

1. 写入日志记录

2. 将STARTCKPT记录写入日志时所有已提交事务已经写到缓冲区但还没有写到磁盘的数据库元素写到磁盘

在写入记录之后,系统需要遍历所有的缓冲区,找出那些已经被已提交事务修改过但尚未写入磁盘的数据库元素(即“脏页”)。然后,将这些脏页写入磁盘。这一步是检查点过程的核心,因为它确保了所有在检查点之前已经提交的事务的修改都被持久化到磁盘上。

3. 写入日志记录并刷新日志

在所有必要的脏页都被写入磁盘之后,系统在redo日志中写入一个记录,标志着检查点过程的结束。

6.3.4 使用带检查点 redo 日志的恢复

** 情况一:最后一个检查点记录是**

<STARTCKPT(T,…,T)>记录之前提交的所有事务的修改都已经成功写入了磁盘。因此,恢复过程可以简化为:

忽略<STARTCKPT(T,…,T)>之前提交的事务,因为它们的修改已经持久化。

关注<STARTCKPT(T,…,T)>中列出的事务以及在该检查点之后开始的所有事务。

扫描日志,从日志的开头或上一个检查点之后开始,直到找到<STARTCKPT(T,…,T)>。

重做(redo)<STARTCKPT(T,…,T)>中列出的事务以及之后开始并提交的所有事务的修改。这些事务的修改可能还没有写入磁盘。

停止扫描日志,当遇到最早的记录(表示一个新事务的开始)且该事务不在<STARTCKPT(T,…,T)>中时,因为这意味着后续的事务与当前恢复过程无关。

情况二:最后一个检查点记录是<START CKPT (T,…,Tk)>

我们不能确定在<START CKPT (T,…,Tk)>之前提交的事务的修改是否已经被写入磁盘。因此,恢复过程需要更多的步骤:

回溯到日志中查找最近的记录。

找到与这个匹配的<STARTCKPT(S,…,Sn)>记录。

重做(redo)所有在<STARTCKPT(S,…,Sn)>和<START CKPT (T,…,Tk)>之间开始并成功提交的事务的修改,因为这些事务的修改可能还没有被写入磁盘。

检查<START CKPT (T,…,Tk)>之后的事务,并只重做那些已经提交的事务的修改。

6.4 undo/redo 日志

我们已经看到了两种不同的日志方式,它们的差别在于当数据库元素被修改时日志中保存旧值还是新值。它们各有其缺陷:
  • undo日志要求数据在事务结束后立即写到磁盘,可能增加需要进行的磁盘I/0次数。
  • 另一方面,redo日志要求我们在事务提交和日志记录刷新以前,将所有修改过的块保留在缓冲区中,这样可能增加事务需要的平均缓冲区数。
  • 如果数据库元素不是完整的块或块集,在检查点过程中undo日志和redo日志在如何处理缓冲区方面都存在矛盾。例如,如果一个缓冲区中包含被提交的事务修改过的数据库元素A和同一缓冲区中被尚未将其COMMIT记录写到磁盘的事务修改过的数据库元案B。

6.4.1 undo/redo 规则

规则UR 1 :在事务T对数据库元素X的修改(即新值w写入磁盘上的X)之前,更新记录

6.4.2 使用 undo/redo 日志的恢复

** 1. 按照从前往后做顺序,重做所有已提交的事务**

这一步是在系统恢复过程中首先执行的。它的目的是重新应用所有已经成功提交的事务的修改,以确保这些修改对数据库的影响是持久的。

2. 按照从后往前做顺序,撤销所有未提交的事务

在重做所有已提交事务之后,下一步是撤销所有未提交的事务。这是因为未提交的事务表示它们还没有被系统正式接受为数据库状态的一部分,因此它们的修改不应该在恢复后的数据库中反映出来。为了撤销这些事务,系统会按照日志中从后往前的顺序(即事务发生的逆序)来查找所有未包含日志记录的事务,并撤销它们所做的修改。

6.4.3 undo/redo 日志的检查点

1. 写入日志记录

2. 将所有脏缓冲区写到磁盘

3. 写入日志记录 <END_CKPT> 并刷新日志

6.5 针对介质故障的防护

6.5.1 备份

** 完全转储**

完全转储涉及拷贝数据库在某一时刻的完整状态。这是恢复过程的基础,因为它提供了一个完整的、无遗漏的数据集。完全转储通常被标记为“0级”转储,因为它不依赖于任何先前的转储。

增量转储

增量转储仅拷贝自上次转储(无论是完全转储还是增量转储)以来发生变化的数据库元素。这种方法显著减少了每次备份所需的数据量,但恢复过程可能需要多个转储文件,因为它们是按顺序依赖的。增量转储可以进一步细分为多个级别,其中“i级”转储表示拷贝自最后一个小于或等于i级转储之后改变的所有内容。

起始点:恢复过程通常从一个完整的转储(0级转储)开始,因为这个转储包含了数据库在某个时间点的完整状态。

增量层叠:随后的增量转储(1级、2级、…、i级)都是基于之前的转储进行的。例如,1级增量转储包含了自0级转储以来发生变化的元素;而2级增量转储则包含了自最近一次1级(或更低级别)增量转储以来发生变化的元素,以此类推。

恢复顺序:在恢复时,必须首先恢复0级转储,因为这是所有后续增量转储的基础。然后,需要按照递增的顺序(1级、2级、…、i级)应用增量转储,以确保所有更改都被正确地应用到数据库中。

6.5.2 非静止转储

非静止转储:是一种在数据库系统不停止运行的情况下进行的备份策略。由于数据库在备份过程中仍然处于活动状态,新的事务可能会开始、修改或提交,因此备份的数据库状态可能会与转储开始时有所不同。为了解决这个问题,非静止转储结合了日志记录来确保数据库在恢复时能够达到一个一致的状态。

非静止转储的特点

并发性:在备份过程中,数据库系统继续处理新的事务,包括数据的插入、更新和删除。

日志依赖:由于备份是在数据库活动的状态下进行的,因此备份中可能包含不一致的数据。为了恢复到一个一致的状态,需要依赖于在备份过程中生成的日志记录。

顺序拷贝:非静止转储通常按照某种固定的顺序(如数据库表的顺序或数据页的顺序)来拷贝数据库元素。然而,由于并发性,这些元素在被拷贝的过程中可能会被其他事务修改。

恢复过程:恢复过程包括将备份数据恢复到数据库,并应用备份过程中生成的日志记录来纠正任何不一致。这通常涉及回滚未提交的事务并应用已提交事务的更改。

6.5.3 使用备份和日志的恢复

假设发生了介质故障,并且我们要通过此前已到达安全的远程结点、在崩溃中未丢失的日志和最近的备份重建数据库。我们执行下列步骤:

1.根据备份恢复数据库。

a)找到最近的完全转储,并根据它来恢复数据库(即将备份拷贝到数据库)。

b)如果有后续的增量转储,按照从前往后做的顺序,根据各个增量转储修改数据库。

2.用保留下来的日志修改数据库。使用对应所用日志方式的合适的恢复机制。

第7章 并发控制

调度器:不同事务各个步骤的执行顺序由调度器完成。

调度器所要实现的:可串行性,或者冲突可串行性。

调度器最重要的技术:封锁,时间戳,有效性确认。

7.1 串行调度和可串行化调度

7.1.1 调度

重要的读写动作发生在主存缓冲区中,而不是磁盘上。

调度是一个或多个事务的重要动作的一个序列。

7.1.2 串行调度

一个事务的所有动作完成之后才能进行下一个事务的所有动作。不同事务的调度顺序结果可能不一样。

7.1.3 可串行化调度

** 可串行化调度** :它允许事务并发执行,但产生的结果与某个串行调度产生的结果相同。

可串行化:如果一个并发调度(即多个事务可能同时执行)的执行结果与某个串行调度(即事务按某种顺序一个接一个执行)的执行结果对于所有可能的数据库初始状态都是相同的,那么这个并发调度就被认为是可串行化的。

7.1.5 事务和调度的一种记法

** 读操作 (r** **Ti**** (X))** : 表示事务读取数据库元素X的当前值。

写操作 (wTi(X)): 表示事务将数据库元素X的值更改为新值。

7.2 冲突可串行化

7.2.1 冲突

1. 同一事务的两个动作![](https://img-blog.csdnimg.cn/img_convert/234279b6faed932fb17493f39f0a8633.png) 总是冲突的。单个事务的动作顺序不能改变。 2. 不同事务对同一数据库元素的写冲突。 3. 不同事务对同一数据库元素的读和写也冲突。

总结

不同事务的任何两个动作可以交换,除以下情况外:

  1. 它们涉及同一数据库元素。
  2. 至少有一个是写。

冲突等价

如果通过一系列相邻动作的非冲突交换能将它们中的一个转换为另一个,我们说两个调度是冲突等价的。

冲突可串行化

如果一个调度 冲突等价 于一个串行调度,那么我们说该调度是冲突可串行化的。

7.2.2 优先图及冲突可串行化判断

** 冲突可串行化:** 它确保了一个调度虽然可能包含并行执行的事务,但这些事务的执行顺序可以重新排列成一个没有冲突的串行执行顺序,同时保持所有事务的原始读写操作。如果这样的串行顺序存在,那么该调度就被称为冲突可串行化。

优先图

优先图是一种有向图,用于表示事务之间的先后顺序关系。

根据调度序列,与r冲突的是w,与w冲突的是r,w。

例:

  1. 与r2(A)冲突的是w1.3(A),因此存在一条 2——>3
  2. 与r1(B)冲突的是w2.3(B),因此存在一条 1——>2
  3. 与w2(A)冲突的是w1.3(A),r1.3(A),因此存在一条2——>3
  4. 与r2(B)冲突的是w1.2(B),因此存在一条2——>1

以此类推…得到优先图:

7.3 使用锁的可串行化实现

事务获得在它所访问的数据库元素上的锁,以防止其他事务几乎在同一时间访问这些元素并因而引人非可串行化的可能。

7.3.1 锁

** 读写前的锁请求:**

事务T在读取(r(X))或写入(w(X))数据库元素X之前,必须先请求(L(X))并获得该元素上的锁。这确保了当事务尝试访问数据库元素时,它对该元素具有独占访问权,从而避免了数据的不一致性问题。

锁请求(L(X))和读写操作(r(X)或w(X))之间不能有释放该元素锁的操作(u(X))。这是为了确保在访问元素期间,锁是持续有效的。

锁的释放:

事务T在完成对数据库元素X的读写操作后,必须释放(u(X))在该元素上的锁。这允许其他事务在需要时能够请求并获取锁,进而访问或修改该元素。

锁的互斥性:

任何两个事务(T1和T2)都不能在同一时间封锁同一个数据库元素X,除非其中一个事务(如T1)已经先释放了它在X上的锁。这意味着如果调度中存在两个连续的锁请求动作L(X)和L(X’)(假设它们是由不同事务发出的),那么在这两个动作之间必须有一个对应的解锁动作u(X)(由已经持有锁的事务执行)。确保了数据库元素在任何时候都只能被一个事务独占访问,从而维护了数据的一致性和完整性。

7.3.3 两阶段封锁

** 加锁阶段:**

在此阶段,事务可以申请并获得它需要的所有锁

一旦事务开始了加锁阶段,它就不能释放任何锁,直到进入解锁阶段。

解锁阶段:

在此阶段,事务可以释放它在加锁阶段获得的所有锁

一旦事务进入了解锁阶段,它就不能再申请任何新锁。

7.4 有多种锁模式的封锁系统

在7.3节讨论的简单封锁模式中,每个事务在读写数据库元素时都需要获得锁,这在实际应用中可能过于严格且效率低下。
  • 事务T即使只想读数据库元素X而不写它,也必须获得X上的锁。

7.4.1 共享锁与排他锁

** 共享锁(Shared Lock,S锁):**

也称为读锁。

当事务需要读取数据库元素但不修改它时,可以请求共享锁。

多个事务可以同时持有同一个数据库元素上的共享锁,这意味着它们可以同时读取该元素。

如果某个事务已经持有数据库元素的共享锁,其他事务也可以请求该元素的共享锁,但不能请求排他锁。

排他锁(Exclusive Lock,X锁):

也称为写锁。

当事务需要修改数据库元素时,必须请求排他锁。

排他锁是独占的,即在同一时间只有一个事务可以持有某个数据库元素上的排他锁。

如果某个事务已经持有数据库元素的排他锁,其他事务既不能请求该元素的共享锁也不能请求排他锁

:::color2
sli(X):事务Ti申请数据库元素X上的一个共享锁

xli(X):事务Ti申请数据库元素X上的一个排他锁

li(X):请求锁

ui(X):释放锁

:::

7.4.2 相容性矩阵

行:表示数据库元素上当前已经被其他事务持有的锁类型。

列:表示新申请的锁类型。

示例

S 申请X 申请
S 持有
X 持有

7.4.3 锁的升级

在数据库事务管理中,锁机制是确保数据一致性和隔离性的重要手段。锁升级是指事务在开始时获取较低级别的锁(如共享锁),随着操作的进行,根据需要将其升级到更高级别的锁(如排他锁)。

优点:

提高并发性:在事务开始时使用共享锁可以允许多个事务同时读取数据,从而提高了系统的并发性能。

减少等待时间:事务在需要写入数据前可能已经完成了大部分读取操作,此时升级锁可以减少因等待排他锁而导致的延迟。

缺点:

死锁风险增加:多个事务可能同时尝试升级同一资源的锁,导致它们相互等待对方释放锁,从而引发死锁。

出现死锁:

当T和T’几乎同时开始时,它们都会首先获取A上的共享锁。

随后,它们都试图将A上的锁升级为排他锁,但由于对方持有A上的共享锁,因此都无法立即升级。

结果是,T和T’都会陷入无限等待状态,因为它们都在等待对方释放A上的锁。

死锁解决方案

使用超时机制:在尝试升级锁时设置超时时间,如果在规定时间内无法升级锁,则释放已持有的锁并回滚事务或采取其他恢复措施。

7.4.4 更新锁

更新锁介于共享锁(S锁)和排他锁(X锁)之间。更新锁允许事务在读取数据的同时,保留将来可能对该数据进行更新的权利。

更新锁的特点

读取权限:持有更新锁的事务可以读取数据,但不能写入数据。

升级权限:更新锁可以被升级为排他锁,但共享锁不能直接升级为排他锁(除非先释放共享锁)。

互斥性:一旦资源上有了更新锁,就不能再有其他事务在该资源上获得任何类型的锁(包括共享锁、更新锁和排他锁),直到更新锁被释放或升级为排他锁。

行:表示数据库元素上当前已经被其他事务持有的锁类型。

列:表示新申请的锁类型。

避免死锁

在例7.16中,由于事务T和T’都试图将共享锁升级为排他锁,导致它们互相等待对方释放锁,从而引发死锁。通过使用更新锁,我们可以避免这种情况。在例7.17中,事务T和T’都首先申请A上的更新锁。由于更新锁的互斥性,当T持有A上的更新锁时,T’尝试获取A上的更新锁将会被拒绝。这样,T可以继续执行并完成其操作,然后释放A上的锁。之后,T’可以获取A上的更新锁,进而升级为排他锁并完成其操作。

7.4.5 增量锁

增量锁是一种特殊的锁机制,适用于那些仅涉及对数据库元素进行增加或减少操作的事务。

多个事务可以同时对同一数据库元素进行增量操作,而这些操作的结果与它们执行的顺序无关。

增量锁的特性

增量操作的交换性:增量锁允许多个事务同时对同一数据库元素进行增量操作,且这些操作的顺序可以交换而不影响最终结果。

锁的兼容性:在增量锁被持有的情况下,其他事务不能对该元素加共享锁(S锁)或排他锁(X锁),但可以同时有多个事务持有该元素的增量锁(i锁)。

限制读写操作:增量锁不赋予事务对数据库元素进行读或写操作的权力,仅允许进行增量操作。

7.5 封锁调度器的一种体系结构

事务申请锁,释放锁都是调度器的干预。

7.5.1 插入锁动作的调度器

事务请求的动作通常通过调度器传送并在数据库上执行。但是在某些情况下,事务等待个锁而被推迟,其请求(暂时)不被传送到数据库。

第I部分:请求处理与锁插入

功能:负责接收来自事务的请求流,这些请求包括数据库访问操作(如读、写、增量、更新)和必要的锁请求。

操作:在数据库访问操作之前,插入适当的锁动作(如加锁、解锁等)。然后,将处理后的封锁和数据库访问动作序列传递给第Ⅱ部分。

第Ⅱ部分:动作执行与锁管理

功能:接收第I部分传来的封锁和数据库访问动作序列,并负责它们的执行。

操作

  1. 如果一个事务由于等待锁而被推迟,则将该动作推迟,并加入到一个待执行列表中。
  2. 如果事务的所有请求锁都已被授予,则执行该事务的数据库访问操作或封锁动作。
  3. 封锁动作的执行包括检查锁表,以确定锁是否可以被授予。如果可以,则更新锁表;如果不可以,则在锁表中标记该锁已被申请,并推迟事务直到锁可用。

锁释放与通知:当事务提交或中止时,通知第I部分释放锁。如果有事务等待这些锁,则通知第Ⅱ部分进行处理。

锁的获得与执行:当某个数据库元素上的锁变得可用时,第Ⅱ部分决定哪个(或哪些)事务可以获得这些锁,并允许它们执行被推迟的操作,直到它们完成或遇到新的锁等待。

7.5.2 锁表

![](https://img-blog.csdnimg.cn/img_convert/711d202a2af833c21385a652860a4193.png)

锁表是将数据库元素与有关该元素的封锁信息联系起来的一个关系表。

表可以用一个散列表来实现,使用数据库元素(地址)作为散列码。任何未被封锁的元素在表中不出现,因此表的大小只与被封锁元素的数目成正比,而不是与整个数据库的大小成正比。

一个列表描述所有或者在A上当前持有锁,或者在等待A上的锁的那些事务。

组模式概括事务申请A上的一个新锁时所面临的最苛刻的条件。

在共享-排他-更新(SXU)封锁模式中,规则很简单:组模式:

a)S表示被持有的只有共享锁。

b)U表示有一个更新锁,而且可能有一个或多个共享锁。

c)X表示有一个排他锁,并且没有其他的锁。

封锁请求处理

检查锁表项:

  1. 如果A的锁表项不存在,说明A上当前没有锁,调度器将创建一个新的锁表项,并立即同意T的请求。
  2. 如果A的锁表项存在,调度器将检查当前的组模式(U-更新锁、X-排他锁、S-共享锁)。

根据组模式决定请求:

  1. 如果组模式是U(更新锁),则只有T自己持有的U锁或与其他请求相容的锁才能被授予。否则,请求被拒绝,并在等待列表中为T的请求添加一项,设置Wait?=Yes。
  2. 如果组模式是X(排他锁),则请求同样被拒绝,并添加等待项。
  3. 如果组模式是S(共享锁),则另一个共享锁或更新锁可以被授予。如果授予的是更新锁,则组模式改为U;如果是共享锁,则组模式保持S。

更新锁表:

  1. 无论锁是否被授予,新的列表项都会通过Tnext和Nex字段正确地链接到等待列表中。
  2. 调度器可以从锁表直接获取所需信息,无需检查锁的列表(但列表用于管理等待的事务)。

解锁处理

删除列表项:

从等待列表中删除T关于A的项。

检查并更新组模式:

如果T持有的锁与组模式不同(如T持有S锁,但组模式是U),则组模式保持不变,因为还有其他事务可能持有U锁。

如果T的锁与组模式相同,并且T是最后一个持有该锁的事务(即没有其他事务持有A上的锁了),则组模式可能需要更改:

如果组模式是U,并且没有其他锁存在,则组模式变为无(因为没有锁了)。

如果组模式是S,并且还有其他事务持有S锁,则组模式保持S;否则,也变为无。

授予等待的锁:

如果存在等待的锁(Waiting = ‘yes’),调度器需要决定如何授予这些锁。策略包括:

先来先服务:按照请求的先后顺序授予锁,确保公平性。

共享锁优先:首先授予所有等待的共享锁,然后是更新锁,最后是排他锁。这种策略可能导致更新锁或排他锁饿死。

升级优先:如果有一个持有U锁的事务在等待将其升级到X锁,则优先授予该锁。这可以确保重要的更新或写入操作尽快完成。

7.6 数据库元素的层次

7.6.1 多粒度的锁

** 元组级锁(最细粒度):**

优点:支持高并发,因为每个事务只需要锁定它所修改或查询的元组。

缺点:管理开销大,因为锁的数量可能非常多,同时增加了死锁的风险。

适用场景:当大量事务主要修改或查询少量的数据时,如银行的存款和取款操作,每个账户视为一个元组。

页/块级锁:

优点:相对于元组级锁,减少了锁的数量和管理开销,同时仍然可以支持较高的并发。

缺点:可能会因为锁定整个页/块而导致一些不必要的等待,尤其是当页/块内只有少量数据被修改时。

适用场景:当事务倾向于修改或查询页/块内多个数据时,如银行数据库中同一页内的多个账户。

关系级锁(最粗粒度):

优点:管理开销最小,因为整个关系只需要一个锁。

缺点:并发性能低,因为任何对关系内数据的修改都需要先获得整个关系的锁。

适用场景:当大多数事务是读操作且很少写操作时,如文档数据库,其中文档经常被检索但很少被编辑。

7.6.2 警示锁

。警示锁与普通锁(如共享锁S和排他锁X)配合使用,以提供更灵活的锁控制机制,特别是在处理多层次数据结构时。

警示锁的基本概念

定义:警示锁通过在普通锁前加前缀(如“意向”)来表示,例如IS(Intention Shared)和IX(Intention Exclusive)。它们用于表明事务打算在其子元素上获取特定类型的锁。

作用:警示锁主要用于提高锁管理的效率,减少不必要的锁冲突,并帮助数据库系统快速判断某个事务是否可能与其他事务发生冲突。

警示锁的协议规则

从根开始:在尝试对任何元素加S或X锁之前,必须从层次结构的根(如关系)开始。

直接加锁:如果当前结点就是需要封锁的结点,则直接请求该结点上的S或X锁。

向下传递警示:如果需要封锁的结点位于层次结构中更靠下的位置,则在当前结点上添加一个警示锁(IS或IX)。然后,继续向包含目标结点的子结点行进,并重复此过程。

锁的相容性矩阵

7.6.3 幻象与插入的正确处理

幻象问题是指在一个事务执行过程中,另一个事务插入了新的行,这些新行满足第一个事务中某个查询的条件,但第一个事务在开始时并未看到这些行。这会导致第一个事务的查询结果不准确,违反了事务的隔离性。

解决方案

使用更严格的隔离级别:如可串行化(Serializable)隔离级别。在这个级别下,事务会完全串行执行,从而避免了幻象的发生。但这种方法可能会显著降低系统的并发性能。

使用锁:

表级锁:在事务开始时,对整个表加锁,直到事务结束。这可以防止其他事务插入新行。但这种方法会限制表的并发访问。

范围锁:对查询涉及的数据范围加锁,防止其他事务在该范围内插入新行。但这种方法需要数据库系统能够预测查询的范围,这在很多情况下是不可行的。

幻象锁(Phantom Locks):一种特殊的锁,用于防止在特定查询条件下插入新行。虽然大多数数据库系统不直接提供这种锁,但可以通过其他机制(如索引锁、谓词锁等)来实现类似的效果。

使用多版本并发控制(MVCC):MVCC 允许数据库为每个事务维护数据的一个或多个版本。当事务读取数据时,它看到的是事务开始时数据的一个快照。这样,即使其他事务插入了新行,也不会影响当前事务的查询结果。

7.7 树协议

7.7.1基于树的封锁的动机

在处理B-树索引的并发访问时,传统的两阶段封锁(2PL)协议可能会遇到严重的限制,导致并发性能不佳。这是因为B-树的结构特性使得根节点或内部节点的锁可能成为瓶颈,特别是在进行插入或删除操作时,理论上可能需要重写这些节点

基于树的封锁动机

减少锁粒度:直接在B-树的根节点或所有内部节点上加锁会极大地限制并发性,因为这会要求事务在继续深入树之前获得这些高级别节点的锁。通过基于树的结构特性来优化锁的使用,可以减少不必要的锁等待和锁冲突。

提高并发性:通过观察和预测B-树操作的局部性,可以设计一种机制,使得事务在确信不会修改树的高层结构时,能够提前释放高级别节点的锁。这种策略可以显著增加多个事务同时访问B-树的能力。

保持可串行性:虽然释放锁可能违反了两阶段封锁的原则,但可以通过设计专门的协议来确保事务的调度仍然是可串行化的。这些协议利用了B-树操作的特定顺序性和结构稳定性,通过跟踪事务对树的访问路径和所做的修改来确保数据的一致性。

7.7.2 访问树结构数据的规则

** 事务的第一个锁可以在树的任何结点上:**

这个规则允许事务从树的任何位置开始其操作,而不仅仅是从根节点开始。这增加了灵活性,因为事务可以根据其需要直接定位到树中的相关部分。

只有事务当前在父结点上持有锁时才能获得后续的锁:

这个规则确保了事务在深入树结构时遵循一种“从上到下”的顺序。这有助于防止不同事务之间的锁冲突,因为它们在树中的路径不会交叉。此外,这个规则还隐含了一个要求,即事务在释放一个节点的锁之前,必须先释放其所有子节点的锁。这是因为如果没有这个要求,事务可能会陷入一个无法释放父节点锁的情况,因为它还持有子节点的锁。

结点可以在任何时候解锁:

这个规则允许事务在不再需要某个节点的锁时立即释放它。这有助于减少锁持有的时间,从而提高并发性。

事务不能对一个它已经解锁的结点重新上锁,即使它在该结点的父结点上仍持有锁:

这个规则防止了事务在释放了一个节点的锁之后重新获取它,除非它重新开始整个事务或重新获得从根节点到该节点的路径上所有节点的锁。这有助于避免死锁和复杂的状态管理问题。

7.8 使用时间戳的并发控制

7.8.1 时间戳

** 基于系统时间生成:**

这种方法利用操作系统的当前时间来为事务分配时间戳。

使用计数器生成:

调度器维护一个递增的计数器,每当一个新事务开始时,计数器加1并将新值作为事务的时间戳。

RT(X):X的读时间戳。表示最后一次成功读取X的事务的时间戳。

当事务T读取X时,如果T的时间戳大于RT(X),则更新RT(X)为T的时间戳。

WT(X):X的写时间戳。表示最后一次成功写入X的事务的时间戳。

当事务T写入X时,无论T的时间戳与WT(X)的关系如何,都更新WT(X)为T的时间戳。

C(X):X的提交位。表示最后一次写入X的事务是否已经提交。

7.8.2 事实上不可实现的行为

** 过晚的读**

事务T试图读取一个在其理论上执行之后才被写入的值。这违反了时间戳调度器的假设,即事务的执行顺序应该与其时间戳顺序一致。

我们只能中止事务T。

过晚的写

如果事务的时间戳处于一个“过晚”的位置,即它试图写入一个已经被其他事务读取(但尚未写入新值)的数据库元素,并且这个读取操作的时间戳比当前事务的时间戳还要晚,那么就发生了过晚的写。

7.8.3 脏数据的问题

** 脏读问题**

脏读发生在一个事务读取了另一个未提交事务修改的数据。但事务最终可能由于某种原因而未能提交。

解决方法

检查提交位C(X),如果C(X)为假,表示X的当前值尚未被提交,因此事务T应该推迟读取X,直到事务U提交或中止。

另一潜在问题

事务T试图写入X,但发现有一个时间戳更晚的事务U已经写入了X,T继续写的话会造成数据错误。

解决方法:Thomas写法则

Thomas写法则允许一个事务在发现有更晚时间戳的写操作已经发生时,跳过自己的写操作。

Thomas写法则的****潜在问题

如果事务U后来中止,其写入X的操作应该被撤销,但此时T的写操作已经被跳过,无法恢复X到T写入之前的状态。这可能导致数据丢失或不一致。

处理策略

尝试性写入:当事务T写入数据库元素X时,调度器并不立即将X的值更新为T写入的值,而是将这次写入视为“尝试性”的。调度器同时保存X的旧值和原有写时间戳WT(X)的拷贝。

提交或中止处理:

如果事务T提交,调度器将X的值更新为T写入的值,并将提交位C(X)设为真。

如果事务T中止,调度器将撤销T对X的写入,恢复X的旧值和原有写时间戳WT(X)。

7.8.4 基于时间戳调度的规则

每个事务分配一个唯一的时间戳(TS)。
1. 读请求的处理
当调度器收到来自事务T的对数据项X的读请求r(X)时:
  • 如果TS(T) ≥ WT(X),即事务T的时间戳大于或等于X的最近写时间戳,说明T的读请求是事实上可实现的(即不会读到脏数据)。
    • 如果C(X)为真(表示X的最近写已提交),则同意请求,并根据需要更新X的读时间戳RT(X)。
    • 如果C(X)为假(表示X的最近写未提交),则推迟T直到C(X)为真或写X的事务中止。
  • 如果TS(T) < WT(X),即事务T的时间戳小于X的最近写时间戳,说明T的读请求是事实上不可实现的(即会读到脏数据),因此必须回滚T并重启。
2. 写请求的处理
当调度器收到来自事务T的对数据项X的写请求w(X)时:
  • 如果TS(T) ≥ PT(X) 且 TS(T) ≥ WT(X),即事务T的时间戳大于或等于X的最早提交时间戳和最近写时间戳,说明T的写请求是事实上可实现的,即T写X之后暂时不会被其他写覆盖,必须执行。
    • 为X写入新值。
    • 更新WT(X)为TS(T)。
    • 将C(X)设为false,表示X的最新写尚未提交。
  • 如果TS(T) > RT(X) 但 TS(T) < WT(T),即T的时间戳大于X的读时间戳但小于X的最近写时间戳,说明T的写是事实上可实现的但X已有更晚的写。
    • 如果C(X)为真,则前一个X的写已提交,忽略T的写。
    • 如果C(X)为假,则推迟T直到C(X)为真或写X的事务中止。
  • 如果TS(T) < RT(X),即事务T的时间戳小于X的读时间戳,说明T的写是事实上不可实现的,必须回滚T。
3. 提交请求的处理
当调度器收到事务T的提交请求时:
  • 将T所写的所有数据库元素X的C(X)设为true,表示这些写已提交。
  • 允许任何等待X被提交的事务继续进行。
4. 中止或回滚请求的处理
当调度器收到事务T的中止请求或决定回滚T时:
  • 任何等待T所写元素X的事务需要重新尝试其读或写请求,检查这些操作在T的写被中止后是否仍然合法。

7.8.5 多版本时间戳

每个数据库元素(如记录、块或页)都可能有多个版本,每个版本都与一个特定的时间戳相关联。这些版本记录了数据在不同时间点的状态。

多版本时间戳的主要目的是允许事务在读取数据时,即使其他事务正在修改这些数据,也能继续执行而不会导致中止。这是通过让事务读取适合其时间戳的数据版本来实现的。

7.8.6 时间戳与封锁

** 时间戳机制:**

在高冲突场景下,由于多个事务尝试写入相同数据,可能导致大量事务需要回滚,增加系统延迟。

适用于大多数事务为只读或并发事务较少读写同一数据元素的情况。

封锁机制:

事务可能因等待锁而阻塞,导致性能下降,甚至可能出现死锁。

在高冲突场景下,即多个事务频繁读写相同数据元素时,封锁机制的性能较好。

7.9 使用有效性确认的并发控制

有效性确认与时间戳的主要区别在于调度器维护关于活跃事务正在做什么的一个记录,而不是为所有数据库元素保存读时间和写时间。事务开始为数据库元素写人值前的一刹那,它经过一个“有效性确认阶段”,这时用它已经读的和将写的元素集合与其他活跃事务的写集合做比较。如果存在事实上不可实现行为的风险该事务就被回滚。

7.9.1 基于有效性确认调度器的结构

告知调度器事务T的读集合RS(T)和写集合WS(T)

读阶段:

事务读取其读集合(RS(T))中的所有数据库元素。事务在其局部地址空间中计算将要写入的所有值,但此时不修改数据库。

有效性确认阶段:

调度器比较当前事务的读写集合(RS(T)和WS(T))与其他活跃事务(即在START或VAL集合中的事务)的读写集合。

如果发现存在冲突,即当前事务的写集合与其他事务的读集合或写集合有交集,且不满足可串行化的要求,则事务被回滚。

如果确认成功,事务进入下一个阶段。

写阶段:

事务将其写集合(WS(T))中的值写入数据库。

为了支持做出是否确认事务有效性的决定,调度器维护三个集合:

START集合:

包含已经开始但尚未完成有效性确认的事务。对于每个事务T,调度器记录其开始时间START(T)。

VAL集合:

包含已经通过有效性确认但尚未完成写阶段的事务。T确认时间VAL(T)。

FIN集合:

包含已经完成写阶段的事务。T完成时间FIN(T)。

7.9.2 有效性确认规则

规则一:检查读后写冲突
对于所有已经过有效性确认(即在VAL或FIN集合中)且在事务T开始前没有完成的事务U(即满足FIN(U) > START(T)),调度器需要检测是否存在读后写冲突。这种冲突发生在事务T读取了某个数据库元素X之后,而事务U可能(或已经)修改了X的值。
  • 具体检查:检查RS(T) ∩ WS(U) 是否非空。如果非空,说明事务T读取了事务U将要修改的数据库元素,这可能导致事务T读到了旧值,而事务U最终会写入新值,从而破坏了串行顺序的假设。
  • 处理:如果检测到这种冲突,调度器必须回滚事务T,以避免潜在的数据不一致。
规则二:检查写-写冲突
对于所有已经过有效性确认(即在VAL集合中)且在事务T进入其有效性确认阶段前没有完成的事务U(即满足FIN(U) > VAL(T)),调度器需要检测是否存在写-写冲突。这种冲突发生在两个事务都试图修改同一个数据库元素。
  • 具体检查:检查WS(T) ∩ WS(U) 是否非空。如果非空,说明事务T和事务U都计划修改同一个数据库元素,而根据假设的串行顺序,事务U应该在事务T之前完成修改。
  • 处理:如果检测到这种冲突,调度器同样需要回滚事务T,以确保事务的执行顺序与假设的串行顺序一致。

第8章 再论事务管理

8.1 可串行性和可恢复性

如果事务在提交前中止,并且其修改没有被撤销,也可能导致数据库状态不一致。

8.1.1 脏数据问题

如果调度不使用提交位,即T1不等待T2提交就读取T2写入的新值:

T2写B

T1读B进行一系列操作

T2进行非法操作导致T2回滚,导致T1读取的B是脏数据

进而导致T1的操作变为非法

8.1.2 级联回滚

为了解决上述问题,我们需要进行级联回滚。

级联回滚:事务T终止时,必须也要中止所有读入T写的数据的事务。然后递归的中止读了被中止事务写入数据的事务。

我们可以用日志来撤销事务,如果数据还未写回磁盘,可以直接用数据库的硬盘拷贝来恢复数据。

8.1.3 可恢复的调度

如果调度中每一个事务都在它所读取的所有事务提交之后才提交,则该调度是可恢复的。

不可恢复举例:由于T2读取了T1写入的数据,T2应该在T1之后提交。

为了使调度是可恢复的:日志记录到达磁盘顺序必须和它们被写入的顺序一致。

8.1.4 避免级联回滚的调度

为了避免级联回滚,我们应用更严格的条件:ACR(avoid cascading rollback)调度。(避免级联回滚调度)

ACR调度:事务只读取已提交事务写入的数据。

8.1.5 基于锁对回滚的管理

基于锁的不产生级联回滚的方法:严格封锁。

严格封锁:直到事务 提交 或 中止且提交 或 中止日志记录已被刷新到磁盘 之后,事务才允许释放排他锁(或其他允许值发生改变的锁,如增量锁)。

遵循严格封锁的严格调度都是ACR调度,都是可串行化。

**修复缓冲区中数据**:
事务在执行过程中因为某种原因而中止,那么缓冲区中尚未提交到磁盘的修改就需要被撤销,以恢复数据库到事务开始之前的状态。

修复的难度主要取决于数据已块为单位管理,还是已更小的单位管理。

块的回滚
由于对块封锁,其他事务不会对块内数据进行更改。所以系统可以通过简单地忽略缓冲区中块A的新值来实现回滚。并将其重新加入到可用缓冲区池中。
小的数据库元素的回滚
一个缓冲区可能包含多个事务的修改,我们中止其中之一,必须保留其他事务的修改。

1.我们可以从存储在磁盘上的数据库中读取A原来的值,并对缓冲区内容做适当的修改。

2.如果日志是 undo 日志或undo/redo日志,那么我们可以从日志中获得改前值。从崩溃中恢复的代码也可以同样用于“自动”回滚。

3.我们可以为每个日志所做的修改维护一个单独的主存日志,该日志仅在对应事务活跃时保留。旧值可以从这-“日志”中获得。

8.1.6 成组提交

在多个事务的提交过程中,不立即将每个事务的提交记录单独刷新到磁盘,而是将这些提交记录先保存在内存中(通常是日志缓冲区)之后就可以释放锁,然后按照它们被写入日志的顺序批量刷新到磁盘。

8.1.7 逻辑日志

当数据库元素(如块或页)中的变化较小时(如更新一个元组的某个属性或插入/删除一个元组),物理日志需要记录整个块或页的新旧值,这会导致大量的冗余信息。

逻辑日志只记录实际发生的变化,如更新的属性、插入或删除的元组等,从而显著减少了日志的大小和冗余。

逻辑日志允许事务在更新数据库后立即释放锁,只要这些更新被正确地记录在日志中。

8.1.8 从逻辑日志中恢复

** 恢复步骤**

1. 重建崩溃时的数据库状态

a) 找到最近的检查点

b) 遍历日志记录

对于每一条日志记录 <L, T, A, B>:

L 是日志序号,表示这条记录的顺序。

T 是事务标识符。

A 是事务动作,如“插入元组”、“删除元组”等。

B 是动作影响的块。

比较块 B 上的日志序号 N 和当前日志记录的日志序号 L:

如果 N < L,则需要对块 B 执行动作 A(redo),因为该动作在崩溃前未成功应用到块 B。

如果 N >= L,则不需要执行任何操作,因为该动作已经成功应用到块 B。

c) 当遇到事务T开始、提交或中止的日志记录时,更新当前的活动事务集合。

2. 中止未完成的事务

a) 从日志末尾向前扫描

遍历日志,从末尾向前到上一个检查点,找到所有需要被中止的事务 T 的日志记录 <L, T, A, B>。

在块 B 上执行动作 A 的补偿动作,并将补偿动作的执行记录到日志中。

b) 如果需要中止的事务在最近的检查点之前开始,继续向前扫描日志,直到找到该事务的起始记录。

c) 写入中止记录

8.2 死锁

8.2.1 超时死锁检测

对事务活跃时间做出限制,超过限制时间就进行回滚,并且释放该事务的锁和其他资源。

8.2.2 等待图

** 节点** :代表数据库中的事务。

:表示事务之间的依赖关系,一个事务等待另一个事务释放锁的情形。

如果事务A持有数据X的锁,事务B等待在X上加锁,就会在等待图中有一条从B指向A的边。

有环则存在死锁。

8.2.3 通过元素排序预防死锁

为数据库中的所有元素(如数据块)定义一个全局的顺序。

每个事务在请求锁时必须遵循这个顺序。如果事务T已经持有了Aᵢ的锁,并且想要获取Aⱼ的锁,那么它必须确保Aᵢ < Aⱼ(在定义的顺序中)。

因为每个事务都在等待一个序号比它当前持有的锁更大的锁,所以不可能出现Tₙ等待T₁的情况。因此系统不会陷入死锁状态。

8.2.4 通过时间戳检测死锁

为每个事务分配一个唯一的时间戳,只要老事务请求较新的事务持有的锁,较新的事务就被杀死。

等待-死亡方案

每个事务在开始时被赋予一个时间戳。

当事务T需要等待事务U持有的锁时,比较它们的时间戳。

T比U老,则允许T等待U释放锁。

U比T老,则T被视为“死亡”,即T将被回滚。

伤害-等待方案

当事务T需要等待事务U持有的锁时,比较它们的时间戳。

T比U老,则T“伤害”U,要求U回滚并释放锁。

但有一个例外:如果U在“伤害”生效前已经完成并释放了锁,则U可以存活。

U比T老,则T等待U释放锁。

**8.3 长事务**

长事务是需要太长时间因而不允许它们保持其他事务所需要的锁的事务。

8.3.2 saga(系列记载)

Saga 将长事务分解为多个短事务,并通过定义这些短事务之间的依赖关系和补偿机制来确保整个业务过程的一致性和可靠性。

组成

一系列动作:通常是原子性的(即作为一个短事务执行)。

状态图:其结点是动作结点或终止结点:,不存在从中止节点发出的弧。

开始节点:表示 Saga 的起始点。

路径:图中的路径代表 Saga 可能执行的流程。通向 Abort 节点的路径表示需要回滚的失败序列,而通向 Complete 节点的路径表示成功的执行序列。

Saga 的并发控制

短事务的并发控制:每个动作自身都是一个短事务,可以使用传统的并发控制机制(如封锁)来执行。

补偿事务:补偿事务是 Saga 中每个动作的逆操作,用于在 Saga 失败时回滚已提交的动作。仔细考虑,以避免死循环和无法终止的 Saga。

第9章 并行与分布式数据库

9.1 关系的并行算法

9.1.1 并行模型

** 共享内存机器**

每一个处理器可以访问所有处理器的所有内存。也就是说对整个机器,有一个单一的物理地址空间,而不是每个处理器一个地址空间。

P:处理器

共享磁盘机器

每一个处理器有自己的内存,其他的处理器不能直接访问到。磁盘可以由任何一个处理器通过通信网络访问到。

无共享机器(最常用)

所有的处理器都有它们自己的内存和一个或多个磁盘。所有的通信都经过从处理器到处理器的通信网络。

9.1.2/3/4 并行算法

** hash散列**

使用散列函数来将数据分布到多个处理器上。用散列函数确定数据应该存放在哪个处理器或哪个桶。

如果R的元组通过散列函数分布到各个处理器上,那么每个处理器可以独立地对本地的R的元组进行操作,然后将结果合并。

当两个关系R和S的元组使用相同的散列函数分布时,每个处理器可以独立地计算本地R和S的元组的并集、交集或差集,然后将结果合并。

如果R和S使用不同的散列函数分布,则需要将所有元组重新分布到一个统一的散列函数中,再进行操作。

与单处理器算法的性能比较

一元操作(如选择σ)的时间将是单处理器执行该操作时间的1/p。

并行算法可能需要进行更多的磁盘I/O(例如,每个数据块可能需要5次I/O而不是3次),但每个处理器上的时间消耗从3(B®+B(S))降低到5(B®+B(S))/p,对于大的p来说,这是一个显著的提升。

9.2 map-reduce 并行架构

![](https://img-blog.csdnimg.cn/img_convert/b55629e374a22df72f88e88aecd49101.png)

映射(Map)

对输入的每个数据项进行处理,并输出一组键值对(key-value pairs)。这些输出将作为归约(Reduce)函数的输入。

归约(Reduce)

它接收映射函数输出的中间结果,并对其进行进一步的处理以产生最终结果。

归约函数会将与给定关键字相关联的所有值组合成一个列表或进行其他形式的聚合。

归约函数可以充分利用并行性。如果归约操作是可结合和可交换的,则可以在映射进程结束之前就开始归约操作,从而加速整个处理过程。

9.3 分布式数据库

9.3.1 数据的分布

关系的水平分解与垂直分解:

水平分解:将一个关系(如表)按行划分,每个分片(片段)包含原关系的一部分行,并存储在不同的节点上。

垂直分解:将一个关系按列划分,每个分片包含原关系的一个属性子集,并存储在不同的节点上。

9.3.2 分布式事务

在分布式系统中,事务可能涉及多个节点的处理,因此传统的事务模型需要调整。

事务的一个部件希望中止整个事务,而其他部件没有遇到任何问题因而希望提交事务,我们用两阶段提交(2PC)机制来确保在所有参与节点上达成一致的提交或中止决策。

使用封锁技术(如锁表)实现全局封锁,从而维护事务的可串行性。

9.3.3 数据复制要关注的问题:

更新操作需要同步到所有副本,当数据在多个节点上复制时,必须确保所有副本保持一致。

需要根据数据的访问频率和更新频率来决定副本的数量和存储位置。频繁更新的数据可能只需要一个主副本和一个备份,而较少更新的数据则可以在多个节点上复制以提高访问效率。

当网络通信发生故障时,不同节点上的数据副本可能会独立演化。在网络恢复后,需要进行数据同步和协调,以确保所有副本再次保持一致。

9.4 分布式查询处理

9.4.1 分布式连接操作问题

计算R(A, B)和S(B, C)的连接的选择:
  1. 把R的一个副本传送给S所在的站点,然后在S所在的站点上计算连接。
  2. 把S的一个副本传给R所在的站点,然后在R所在的站点上计算连接。

9.4.2 半连接化简

半连接的定义和优势

半连接RS(R⋉S)是指从R中选择那些至少与S中的一个元组在Y(或B)属性上有匹配的元组。把S投影到共同属性上,然后用这个投影与R做自然连接。

半连接减少了数据传输,与处理时间。

9.4.3 多个关系的连接

对于两个以上关系进行连接时:

我们可能需要做多个半连接操作来消除关系中的悬挂元组。

可能不存在有限的半连接操作序列来消除所有的悬挂元组。

有可能识别那些可利用半连接操作在有限步骤内消除悬挂元组的关系模式的集合。这种操作序列我们称之为完全化简。

9.4.4 非循环超图

![](https://img-blog.csdnimg.cn/img_convert/1178fbb1348efbc083a0716d8fee3546.png)

一个关系R(A,E,F)被封闭曲线圈出,被称为一个超边。

耳朵,G消费H:一个超边H的所有节点满足下列情况之一:

1.仅包含在H中

2.也包含在G中,我们称G消费H

:::color2
H(A,E,F),G(A,C,E),F满足1,A,E满足2.满足G消费H

:::

耳朵化简:一个耳朵从超图中简单消除,包括只在耳朵中出现的结点。

非循环超图:若一个超图它以被一序列的耳朵简化化简为一个超边。图9-9就是非循环超图。

9.5 分布式提交

9.5.2 两阶段提交

分布式事务T原子性可能被破坏,使用两阶段提交来解决。

通过协调器与所有参与事务的节点之间的消息交换,确保所有节点要么都提交事务,要么都中止事务,从而维护事务的原子性。

准备阶段

协调器发起准备请求:

协调器在其节点上记录日志。并且向所有参与事务的节点发送prepare T消息。

节点响应准备请求:

每个节点收到prepare T消息后,决定是否可以提交其上的事务部分。

如果可以提交,节点进入预提交状态,执行必要的日志记录(如)并刷新日志到磁盘,以确保即使节点故障也能恢复。并且向协调器发送ready T消息。

如果节点决定中止事务,则记录<Don’tcommit T>日志并向协调器发送don’tcommit T消息。

超时处理:

如果协调器在预定的超时时间内未收到某些节点的响应,则将这些节点视为已发送don’t commit T消息处理。

提交阶段

协调器根据响应决定提交或中止:

如果所有节点都发送了ready T消息,协调器记录日志,并向所有节点发送commit T消息。

如果收到一个或多个don’t commit T消息,协调器记录日志,并向所有节点发送abort T消息。

节点根据协调器的指示提交或中止:

收到commit T消息的节点将提交其上的事务部分,并记录日志。

收到abort T消息的节点将中止其上的事务部分,并记录日志。

9.5.3 分布式事务的恢复

** 当节点恢复时,会检查其日志中事务的最后一个记录项:**

如果日志记录是或,需要根据日志记录来回滚事务。

如果日志记录是,则表明全局决定是中止事务,节点需要回滚事务的局部成分。

如果日志记录是,则节点需要与其他节点或协调器通信,以确定事务的最终状态是提交还是中止。

协调器故障处理:

如果协调器发生故障,系统需要选举一个新的协调器来继续事务的管理。

新协调器选举之后:

- <font style="color:#000000;">结点的日志中有<Commit T>记录,那么原协调器必然已经试图向所有结点发送commit T消息,因而将T提交是安全的。</font>
- <font style="color:#000000;">某个结点的日志中有<Abort T>记录,那么原协调器必然已经决定中止T,因而新协调器下令中止T是安全的。</font>
- <font style="color:#000000;">所有结点上都没有<Commit T>或<Abont T>记录,但至少一个结点的日志中没有<Ready T>。那么,由于记录日志发生在发送对应消息之前,我们知道原协调器不可能已收到这一结点的readyT消息,因而不可能已经决定提交T。对新协调器来说,决定中止T是安全的。</font>
- <font style="color:#000000;">当所有节点的日志中都没有<Commit T>或<Abort T>记录,但都有<Ready T>记录时,新协调器无法直接决定事务的状态。可能需要等待原协调器恢复或采取其他人工干预措施来解决问题。</font>

9.6 分布式封锁

9.6.1 集中封锁系统

指定一个专门的封锁结点来管理所有逻辑元素的锁表,所有锁请求和释放操作都通过单一的封锁结点进行。

但是封锁结点成为单点故障点,一旦该结点崩溃,整个系统的锁服务将不可用,导致所有需要锁的事务都无法继续执行。也可能成为性能瓶颈。

9.6.3 封锁多副本的元素

当数据库元素被复制到多个结点上时,每个副本上的局部锁状态可能不同,这可能导致数据不一致和不可串行化的事务执行。

为了确保确保了数据的一致性和事务的隔离性:如果一个事务想要修改逻辑元素X,它必须在X的所有副本上都获得排他锁。同样地,如果一个事务想要读取X,它可以在X的某些或所有副本上获得共享锁。

9.6.4 主副本封锁

主副本:每个逻辑元素的多个副本中有一个被指定为主副本。 维护一个锁表,用于记录关于逻辑元素的锁状态, 负责维护该逻辑元素的所有锁请求和锁状态。

非主副本:除了主副本之外的其他副本用于读取或备份目的,不直接处理锁请求。

通过将锁管理的责任分散到不同的结点,可以避免中央封锁结点成为性能瓶颈。

9.6.5 局部锁构成的全局锁

在不需要中央锁管理器或主副本的情况下,通过局部锁的集合来模拟全局锁的机制。

n:数据库元素A的副本数。

s:事务获得A上的全局共享锁时必须以共享方式封锁的A的副本数。

x:事务获得A上的排他锁时必须以排他方式封锁的A的副本数。

实现要求:

x > n/2:这个条件确保如果两个事务都试图获得A上的全局排他锁,那么至少有一个副本会被两个事务都请求排他锁,从而避免两个事务同时获得全局排他锁。

s + x > n:这个条件确保如果一个事务持有A上的全局共享锁,而另一个事务试图获得全局排他锁,那么至少有一个副本会被这两个事务分别以共享和排他方式请求锁,从而避免这种冲突。

9.7 对等分布式查找

对等分布式系统(P2P网络)是一种去中心化的网络架构,其中每个节点都拥有数据的一个子集,并且没有集中的索引来指示数据的存储位置。

9.7.2 /3分布式散列的集中式解决方案

** 集中式解决方案**

使用一个或多个中心节点来存储和管理所有关键字-值对的映射关系。中心节点通过散列函数h将关键字K映射到特定的数字序号,然后将关键字-值对(K, V)存储在对应序号的节点上。当客户端需要查询某个关键字对应的值时,它会直接向中心节点发送查询请求,中心节点根据散列函数的结果返回相应的值。

在对等网络中处理大规模关键字-值对集合的分布式散列,传统的集中式方法(使用中心结点保存整个表)在数据规模巨大的时候变得不再可行。因此,需要采用分布式散列技术。

9.7.4/... 带弦的圆(解决分布式散列的算法)

有一个大圆,上面分布着许多结点。每个结点都有一个唯一的标识符(通过散列函数计算得到的数字)。

结点之间通过两种链接相连:

直接链接:每个结点都知道它的直接前驱和后继结点,这是沿着圆顺时针方向相邻的结点。

弦链接(手指表):每个结点还维护一个“手指表”,这个表列出了圆上与其有一定距离( 2的幂次方距离)的结点。这些链接称为“弦”,因为它们不是沿着圆直接相连的,而是“跳过”了一些中间结点。

使用手指表查找

结点N想要查找与关键字K关联的值时,首先计算h(K),然后使用其手指表来快速定位到可能的存储结点。:

从当前结点N开始,检查其手指表,看是否有哪个结点的编号大于或等于h(K)。

如果没有找到,就向当前结点的后继发送查询请求。

重复这个过程,直到找到存储(K, V)的结点,或者确定不存在这样的关键字-值对。

插入新结点

  1. 假设新节点N的哈希值为i,它想要加入DHT网络。如果N知道网络中的任何一个节点(比如N’),N可以向N’询问它的后继节点。

查找指定结点

查找请求从某个节点N发起,该节点想要找到与关键字K(其哈希值为j)相关联的值V。

初始时,当前处理请求的节点是N。

在当前节点上执行查找:

当前节点N首先检查自己的数据是否包含关键字K,h(K)=j

如果没有查找到,则N会查阅其手指表。

N在手指表中查找小于j的最大编号的节点N_finger。

如果找到了这样的节点N_finger,N会向N_finger发送一条消息,请求它代表N查找(K, V)。

此时,N_finger成为新的当前节点,算法从步骤2开始重复执行,直到找到包含关键字K的数据或确定该数据不存在。

  1. N将它的后继设置为N’,并将它的前趋设置为空。
  2. 每个节点都会定期运行稳定性检测。这个检测过程确保了每个节点都正确地知道自己的后继和前趋,并在必要时重新分配数据。

步骤:S——>N——>P

1. <font style="color:#000000;">询问后继的前趋:节点N向它的后继S发送消息,询问S的前趋P。如果P是N(正常情况下),则N知道它的链接是正确的,并进行d。</font>
2. <font style="color:#000000;">如果P不是N,且P位于N和S之间,那么N将P记录为自己的新后继。</font>
3. <font style="color:#000000;">更新后继:根据步骤1的结果,N可能需要更新它的后继S'(可能是S或P)。如果S'的前趋为空,或者N位于S'和它的前趋之间,N会告诉S'它的前趋是N,并更新S'的前趋为N。</font>
4. <font style="color:#000000;">数据重新分配:S'需要将一些数据(即所有哈希值小于或等于N的哈希值的数据项)移送给N。这是为了确保数据按照哈希值在环上正确分布。</font>

结点离开或崩溃

礼貌离开:

结点会通知其前驱和后继它要离开,并将数据转移到后继结点。

崩溃处理:

当一个节点崩溃时,如果该节点上存储的数据没有被复制,那么这些数据就会在网络中变得不可用。一般采用下列方法避免这种情况:

节点复制:将每个(K, V)对复制到三个节点上:原始节点、该节点在环上的前驱节点和后继节点。这样,即使其中一个节点崩溃,其他两个节点仍然持有数据的副本。

聚簇:另一种方法是将节点组织成聚簇,每个聚簇中的节点复制它们彼此的数据。当一个聚簇中的节点崩溃时,其他节点可以接替其工作,并继续提供服务。这种方法提供了更高的容错性和灵活性。

分裂:当聚簇变得过大时,它可能会变得难以管理或影响性能。此时,可以将聚簇分裂成两个在环上邻近的新聚簇。

合并:如果某个聚簇变得过小,它可能会与邻近的聚簇合并,以提高效率和冗余度。

第10章信息集成

10.1 信息集成介绍

10.1.1 为什么要进行信息集成

信息集成能够实现不同数据源之间的数据共享和交互,从而提高数据的整体利用率和价值。

可以提供一个统一的数据访问接口,使得应用可以更加方便地获取所需的数据。

可以将来自不同数据源的数据整合到一个统一的数据仓库中,为企业的决策制定提供全面的数据支持。

10.1.2 异质性问题

整合多个数据库间的异质性问题:包括通信协议、查询语言、数据库模式、数据类型、值表示及语义理解等差异。

10.2 信息集成的方式

10.2.1 联邦数据库系统

需要交互的所有数据库对之间一对一连接。

这些连接允许一个数据库系统D1,以另一个数据库系统D2能理解的术语来查询D2。

如果几个数据库中的每一个都需要与其他n-1个数据库进行交互,则我们必须写(n-1)份代码以支持系统之间的查询。

如图10-1所示。在这个图中,我们看到4个数据库形成了一个联邦。这4个数据库中每一个都需要3个组件。

10.2.2 数据仓库

数据仓库集成架构: 把来自几个数据源的数据抽取出来,合成一个全局模式。

数据存储在数据仓库中,在用户看来它与普通数据库无异。

数据仓库中数据的构造方法至少有两种:

1.数据仓库周期性地对查询关闭并根据数据源中的当前数据进行重建。数据重建每隔一夜进行一次,或间隔时间更长一些。

2.根据自上次数据仓库被更新以后对数据源所做的修改,对数据仓库中的数据进行周期性的更新(例如每个晚上),增量更新。

10.2.3 mediator(中间件)

中间件它集成几个数据源的方式与数据仓库中物化关系集成数据源的方式很相似。但是 mediator不存储任何数据

用户提交查询:用户或应用程序通过mediator的接口提交一个查询请求。

查询分解:mediator分析查询,确定需要从哪些数据源获取数据,并可能将原始查询分解为多个子查询。

分发查询:mediator将子查询发送给对应的包装器(wrappers),包装器是负责与特定数据源交互的组件。

数据源响应:每个包装器将查询发送给其对应的数据源,并接收数据源的响应。

结果收集与组合:mediator收集来自各个包装器的响应,并将这些响应组合成一个统一的结果集。

返回结果:mediator将组合后的结果返回给用户或应用程序。

10.3 基于mediator的系统中的包装器

基于Mediator的系统中,包装器(Wrapper)扮演着至关重要的角色,它们是连接数据源与Mediator之间的桥梁。Mediator是一个中央组件,负责接收来自不同应用或用户的查询请求,将这些请求分解并分发到适当的包装器,收集各包装器返回的结果,并最终将处理后的结果返回给请求者。

10.3.1 查询模式的模板

数据源的模式` Cars(serialNo, model, color, autoTrans, navi, ...)`

Mediator使用的模式为<font style="color:#000000;background-color:rgb(253, 253, 254);">AutosMed(serialNo, model, color, autoTrans, dealer)</font>

为了查询给定颜色的汽车,我们可以设计一个查询模板,其中颜色作为参数。

SELECT serialNo, model, color, autoTrans, 'Dealer1' AS dealer  
FROM Cars  
WHERE color = '$c'

Mediator向包装器发送查询请求时,包装器则将模板中的<u><font style="color:#000000;background-color:rgb(253, 253, 254);">'$c'</font></u>替换为实际的颜色值,并执行生成的查询。

10.3.2 包装器生成器

** 包装器生成器(中间件)** :负责将定义好的查询模板转换成实际的包装器代码。

:::color2
包装器生成器执行步骤:

解析模板集合:生成器解析输入的模板集合,理解每个模板的结构、参数和对应的源查询。

构建查询映射表:然后,创建一个内部表或数据结构,用于存储模板中定义的各种查询模式以及与之相关联的源查询

生成包装器代码:基于解析的模板和构建的查询映射表,生成器生成包装器的实际代码。

:::

包装器驱动器:驱动器(Driver)来管理其与mediator和数据源的交互。

:::color2
接收mediator查询:驱动器负责接收,解析来自mediator的查询请求。

查找匹配的模板:在查询映射表中查找相匹配的模板。使用查询中的参数值来实例化源查询。

执行源查询:驱动器将实例化后的源查询发送到数据源。

收集和处理返回数据:数据源对查询的响应由驱动器收集。

返回结果给mediator:最后,驱动器将处理后的数据返回给mediator。

:::

10.3.3 过滤器

过滤器允许在数据传输过程中对数据进行筛选,从而只传递符合特定条件的数据。

过滤器的作用

比如要查询条件有型号和颜色。有颜色查询的模版。可以先对颜色进行查询。查询后的结果集返回给中间件。中间件进行模型的筛选。

10.3.4 包装器上的其他操作

包装器不仅负责从数据源检索数据,还可以对数据进行一系列的处理,包括过滤、投影、聚集和连接等操作。这些操作可以在数据被发送到mediator之前执行,以减少数据传输量、提高查询效率。

投影

投影是一种选择表中某些列的操作,而忽略其他列。包装器可以在将结果发送给mediator之前,通过修改SELECT子句来执行投影操作。

过滤

过滤操作用于从数据集中移除不满足特定条件的元组。可以减少需要投影的数据量,从而提高效率。

聚集和连接

除了过滤和投影之外,包装器还可以执行更复杂的操作,如聚集(如求和、平均值、最大值等)和连接。这些操作通常需要更多的内存和计算资源,并且可能需要在包装器中进行更多的数据预处理。

10.4 基于能力的优化

基于成本的查询优化:通过估计不同查询计划的执行成本来选择成本最低的查询计划。在数据集成环境中,这种方法的有效性受到限制。

基于能力的优化:核心问题不是查询计划的成本,而是该计划是否真的能够执行。只有在可执行的计划中,我们才会尝试估计成本。

10.4.1 有限的数据源能力问题

许多数据源仅通过Web表单提供查询接口,这限制了用户能够提出的查询类型。用户通常只能根据预定义的字段(如书名、作者等)进行查询,并且无法执行复杂的SQL查询。

10.4.2 描述数据源能力的记号

使用修饰符来指定哪些属性在查询中可以被指定、不被指定、或者必须以特定方式指定。

:::color2
修饰符含义

f (free): 属性可以自由选择是否指定。如果指定了,它可以是任意值。

b (bound): 属性必须被指定一个值,但可以是任意值。

u (unspecified): 属性不得被指定值,但会被包含在查询结果中(如果它是输出的一部分)。

c[S] (choice from set S): 属性必须被指定一个值,且该值必须来自有限集合S。

o[S] (optional, from set): 属性可以不被指定值,或者如果指定了,则值必须来自有限集合S。

带有撇号(')的修饰符表示该属性不会被包含在查询的输出结果中。

:::

Cars(serialNo, model, color, autoTrans, navi)。

  1. 仅通过序列号查询:修饰符为b’uuuu,表示:

serialNo(第一个属性)必须被指定,但不会被包含在输出中(因为带有撇号)。

其他四个属性(model, color, autoTrans, navi)不得被指定,但会作为输出的一部分。

  1. 通过型号、色彩及可选的自动变速箱和导航系统查询,修饰符为ubbo[yes, no]o[yes, no],表示:

serialNo(第一个属性)不得被指定。

model和color必须被指定,但可以是任意值。

autoTrans和navi是可选的,如果指定了,则值必须是yes或no。

10.4.3 基于能力的查询计划选择

给定一个 mediator 上的査询,基于能力的査询优化器首先考虑什么查询它可以在数据源上询问,以协助解答查询。

如果有些查询被问过了并回答了,那么我们就绑定更多的一些属性,重复这一过程,直至以下任一条件成立:

1.已经向数据源询问了足够多的査询,以判定 mediator 查询的所有的条件,从而可以回答查询。这样的一个计划被称为可行的。

2.不能再构造任何形式的有效的数据源查询,然而,我们仍然不能回答 mediator 查询在这种情况下,mediator必须放弃,它被给予了一个不可能的查询。

10.4.4 加入基于成本的优化

当数据源的能力被检查后,mediator的查询优化器还不能结束工作。在找到了可行的计划后,它必须选择其中之一。做一个智能的、基于成本的优化。

10.5优化 mediator 查询

10.5.1 简化的修饰符记号

Chain 算法只关心我们是否已经发现一个变量所有可能的常数值,我们可以在 mediator 查询里将自己限制在b(bound)和f(free)修饰符上。

f (free): f 表示一个属性在查询的当前阶段尚未被绑定到任何具体的值,即它是自由的。

b (bound): b 表示一个属性在查询的当前阶段已经被绑定到了所有可能的常数值。

10.5.2 获得子目标的回答

如何根据子目标的修饰符和数据源上关系的修饰符来确定是否可以从数据源查询获得子目标的回答。

为了确定一个子目标是否可以被数据源查询回答,我们需要比较子目标上的修饰符和数据源上关系的修饰符。

匹配规则如下:

  • 如果数据源修饰符中的yi是b或c[S]的形式,那么子目标修饰符中的xi也必须是b。这是因为数据源要求该参数被绑定,而子目标也必须满足这一要求。
  • 如果子目标修饰符中的xi是f,那么数据源修饰符中的yi不能是输出受限制的(即没有加撇的)。这是因为如果子目标中的参数是自由的,那么数据源上的相应参数也不应该被限制在输出中不显示。
  • 如果数据源修饰符中的yi是u或o[S]中的任意一个,那么子目标修饰符中的xi可以是b或f。这是因为数据源允许这些参数是可选的或来自一个集合,所以子目标中的相应参数可以是自由的或已经被绑定的。

10.5.3 Chain 算法

Chain 算法是子目标查询的贪心算法。这个算法的核心思想是通过逐步解决子目标来构建整个查询的解答。这个过程涉及到对子目标、数据源以及当前已解决子目标的集合的迭代处理。

1. 初始化

  • 子目标修饰符:为每个子目标分配一个修饰符。如果子目标的某个参数在 mediator 查询中已被绑定为常量,则对应位置的修饰符为<font style="color:#000000;">b</font>;否则为<font style="color:#000000;">_</font>(表示未绑定)。
  • 关系X:开始时,X 是一个空的关系,用于存储已经解决的子目标的绑定信息。

2. 选择并解决子目标

算法重复选择并尝试解决一个子目标,直到所有子目标都被解决或无法进一步解决为止。

  • 选择子目标:选择一个尚未解决的子目标。
  • 构建查询:根据子目标的修饰符和当前关系X中的元组,为数据源构建查询。
    • 如果子目标的某个参数是常量或来自X的绑定值,则在查询中使用这些值。
    • 如果数据源修饰符要求某个参数是特定集合中的元素(如<font style="color:#000000;">c[S]</font>),则检查该值是否满足条件。
    • 如果数据源修饰符允许自由值,则可能提供一个常量(如果适用)。
    • 如果数据源修饰符是<font style="color:#000000;">_</font>,则不对该参数提供绑定。
  • 执行查询:将构建的查询提交给数据源,并收集返回的结果。
  • 更新关系X:将查询结果(扩展为与子目标参数匹配的形式)添加到关系X中。

3. 更新子目标修饰符和关系X

  • 更新修饰符:对于每个尚未解决的子目标,检查其参数是否已在关系X中被绑定。如果是,则更新该子目标的修饰符,将对应的参数位置标记为<font style="color:#000000;">b</font>
  • 更新关系X:使用新解决的子目标的结果(即其对应的关系)来更新关系X。这通常涉及到投影操作,以保留对后续子目标有用的变量。

4. 终止条件

  • 如果所有子目标都被成功解决,则算法成功,关系X(或其某个子集)即为查询的答案。
  • 如果存在无法解决的子目标,则算法失败,表示无法找到满足查询条件的解答。

10.5.4 在mediator 上结合并视图

在mediator上结合视图和Chain算法的情况下,如何有效地整合来自不同数据源的信息成为了一个关键问题。

咨询所有数据源

必须查询并汇总所有相关数据源的信息,以确保查询结果的完整性和一致性。只有当所有数据源都返回了相应的结果(或确认没有相关数据),查询才会结束。

尽最大努力

不强制要求所有相关数据源都参与查询过程,而是尝试从尽可能多的数据源中获取数据,并返回查询结果。即使某些数据源无法访问或未返回预期的结果。如果某个数据源没有提供查询所需的信息,查询会忽略该数据源并继续尝试从其他数据源中获取数据。最终,查询会返回通过组合所有可用数据源获得的最大限度的回答。

10.6 以局部作为视图的 mediator

** 以全局作为视图(GAV)**

在GAV模型中,全局数据被看作是一个单一的、统一的视图,这个视图可能由多个数据源的数据组合而成。全局查询是在这个统一的视图上执行的,而mediator负责将这个全局查询分解成多个针对各个数据源的子查询,并将结果合并以形成最终的答案。

以局部作为视图(LAV)

在LAV模型中,mediator不定义一个全局的、统一的视图。相反,它为每个数据源定义了一个或多个表达式,这些表达式描述了数据源能够生成哪些元组,以及这些元组如何与全局谓词(即全局查询中的条件)相关联。

当执行一个全局查询时,mediator会根据这些局部表达式来理解每个数据源能够贡献什么信息。然后,mediator会构建针对每个数据源的查询,这些查询会利用数据源特有的结构和索引来优化查询性能。最后,mediator会合并这些查询的结果来形成最终的答案。

优劣

GAV模型侧重于全局视图的构建和查询的分解,而LAV模型更侧重于理解每个数据源的能力,并据此优化查询的执行。

LAV模型更适合于那些数据源之间差异较大、难以构建统一视图的情况。它允许mediator更灵活地处理每个数据源的特性,从而优化查询性能。

10.6.2 LAV mediator 的术语

1. ** 合取查询(Conjunctive Query)** :由一系列原子子句组成,这些子句通过逻辑与(AND)连接。在LAV调解器中,合取查询用于定义视图和执行查询。 2. ** 全局谓词(Global Predicate)** :这些是在LAV调解器中定义的,用于在查询中表示数据源的谓词。全局谓词作为中介查询的子目标,用于连接不同的数据源。 3. ** 视图谓词(View Predicate)** :这些是用于定义视图的谓词,每个视图都有一个独特的视图谓词作为其名称。 4. ** 扩展(Extension)** :在LAV调解器中,扩展是指将视图谓词替换为相应的全局谓词的过程,以便生成一个涉及全局谓词的合取查询。

10.6.3 扩展解决方案

扩展解决方案:将包含视图谓词的查询解决方案转换为仅包含全局谓词的查询的过程。

变量替换规则

给定一个解决方案S,它包含视图谓词V(a1, a2, …, an)作为子目标,其中a1, a2, …, an可以是变量或常量。

视图V的定义:V(b1, b2, …, bn) ← B

其中B是视图定义的主体,b1, b2, …, bn是头部谓词V的参数,为了将V(a1, a2, …, an)替换为B,我们需要遵循以下规则:

确定局部变量:

局部变量是那些仅出现在主体B中而不出现在头部V(b1, b2, …, bn)中的变量。

替换局部变量:

如果B中的任何局部变量也出现在S的其他部分或B的其他子句中,为了避免变量名冲突,确保替换后的查询在逻辑上是正确的。则这些局部变量需要用新的、在V的定义和S的其余部分中都未出现过的变量来替换。

替换头部变量:

在B的主体中,用a1, a2, …, an替换b1, b2, …, bn。

10.6.4 合取查询的包含

如何确定一个查询E是否“包含”在另一个查询Q中。这意味着E产生的所有答案集都应该是Q答案集的子集。为了实现这一点,我们需要定义一种从Q到E的包含映射函数r,并检查这种映射是否满足特定的条件。

包含映射

Q2属于Q1

10.6.6 发现 mediator 查询的解决方法

当我们面对一个mediator查询Q并需要找到其所有解决方案S时,一个关键挑战是解决方案空间可能是无限的。LMSS(Least Model Size Solution)定理为我们提供了一个重要的限制,它有助于缩小搜索范围。

LMSS定理

如果查询Q有n个子目标,那么任何解决方案的任何答案都能被有着最多n个子目标的解决方案产生。

10.7 实体解析

10.7.1决定是否记录代表一个共同实体

在处理来自多个数据源或同一数据源但可能包含错误和不一致性的记录时,如何判断哪些记录代表同一实体。

编辑距离

是衡量两个字符串相似度的一种方法,通过计算将一个字符串转换成另一个字符串所需的最少编辑操作次数(包括插入、删除和替换字符)来实现。在处理类似人名、地址和电话号码等字段时,编辑距离特别有用,因为它能够容忍一定程度的拼写错误和小差异。

规范化

规范化是预处理步骤,旨在将记录中的值转换为标准格式,以便更容易地进行比较。它可以帮助解决由于缩写、昵称、拼写变化等引起的差异。

10.7.2 合并相似记录

合并相似记录是数据清理和整合的关键步骤,旨在优化数据质量。面对拼写错误、缩写、别名和格式不一致等挑战,需明确合并规则、利用编辑距离和模糊匹配技术处理差异、通过传递性检查和合并历史避免冲突,并适时采用手动审查或专家系统解决复杂情况。这一过程确保了数据的一致性和准确性。

10.7.4 ICAR 记录的 R-Swoosh 算法

"R-Swoosh" 算法是针对大规模数据集处理相似记录合并的一种优化算法,它基于ICAR性质来设计,旨在有效减少相似性比较的次数,从而加快合并过程。

ICAR性质:如果两个记录被视为相似,并且被合并成一个新的记录,那么这个新记录与其他记录的合并结果,将与原始两个记录中任意一个与其他记录的合并结果相似,且这种相似性不会因合并的先后顺序或中间引入的附加信息而改变。

将记录视为图中的节点,相似关系视为节点之间的边。目标是将图中的每个连通分量合并为一个单一记录。

R-Swoosh****实现步骤

初始化:

为每个记录创建一个唯一的标识符(ID),并构建一个空的数据结构(如优先队列或堆)来存储待处理的记录对。

构建初始相似性队列:

基于某种启发式或已有的信息(如哈希表、索引等),初步识别可能相似的记录对,并将它们加入到待处理队列中。

迭代合并:

从队列中取出一个记录对(r, s)。

检查r和s是否真正相似(通过具体的相似性函数)。

如果相似,则合并r和s生成一个新的记录t,并更新相关数据结构(如将r和s的邻接记录标记为与t相似)。

将所有因r和s合并而新生成的、可能相似的记录对(如t与r或s的邻接记录)加入待处理队列。

重复上述过程,直到队列为空。

连通分量合并:

随着合并的进行,图中的连通分量逐渐合并成更大的组件。每个组件最终都会被合并为一个单一记录。

结果输出:

每个连通分量(或“簇”)的合并结果即为最终记录集合中的一个元素。

10.7.6实体解析的其他方法

** 1. **** 非ICAR数据集**

在非ICAR数据集中,由于合并操作不具有独立性,合并的结果可能受到合并过程中引入的附加信息或顺序的影响,因此需要系统地比较所有记录(包括通过合并构建的记录)来找到所有可能的合并情况。

为了控制记录的扩散,定义记录间的支配(dominance)关系是一个有效的方法。这种关系表明一个记录包含了另一个记录的所有信息,从而可以安全地消除被支配的记录。在合并函数构成半格的情况下,支配关系通常基于合并操作的结果来定义。如果合并操作不满足半格的性质,则需要根据具体情况构建支配函数。

2. 聚簇

在某些应用场景中,如实体解析或产品分类,我们更关注于将相似的记录分组到聚簇中,而不是将它们完全合并。聚簇允许我们保留记录之间的差异性,同时突出它们之间的相似性。

3. 分区

对于大型实体辨别问题,完全合并所有相似记录的算法可能由于需要检查每个记录对而变得不可行。在这种情况下,将记录分组(即分区),并在每个分区内寻找相似的记录对。通过多次分区,可以逐步减小每个分区内记录的差异性,从而提高合并或聚簇的效率。

第11 章数据挖掘

物品集合 I:所有可能购买的商品的集合。

购物篮集合 B:所有购物篮的集合,每个购物篮都是物品集合的一个子集。

支持度阈值 S:用于判断一个项集是否频繁的阈值。如果一个项集在至少s个购物篮中出现,则称该项集是频繁的。

关联规则

关联规则是频繁项集挖掘的一个重要应用,它用于发现商品之间的购买关系。

一个关联规则可以表示为{i₁, i₂, …, iₖ} => j,其中i₁, i₂, …, iₖ和j都是物品。这个规则表示,如果顾客购买了i₁, i₂, …, iₖ,则他们很可能也会购买j。

一个有用的关联规则需要满足以下三个条件:

1. <font style="color:#000000;">高支持度:规则左边的项集(即{i₁, i₂, ..., iₖ})必须是频繁的。</font>
2. <font style="color:#000000;">高置信度:在所有包含{i₁, i₂, ..., iₖ}的购物篮中,j出现的概率大于某个阈值(如50%)。</font>
3. <font style="color:#000000;">有趣度:j与{i₁, i₂, ..., iₖ}之间的相关性要显著,即j在包含{i₁, i₂, ..., iₖ}的购物篮中出现的概率要明显高于在任意购物篮中出现的概率。</font>

11.2 发现频繁项集的算法

11.2.1频繁项集的分布

** 支持度阈值** :如果设置得太低,几乎所有项集都是频繁的,这会导致太多无用的信息。典型地,支持度值经常被设置为购物篮总数的1%。

频繁项集的大小:大多数频繁项集的规模较小,项集越大,成为频繁项集的概率越小。至少需要包含两个商品的频繁项集。

11.2.2 寻找频繁项集的朴素算法

对于所有购物篮,我们对它的商品进行两重循环,然后对这个购物篮中的所有商品对,我们将该商品对的计数加一。

面临的问题是如何在主存中存储计数值。

计数方法的选择

三角矩阵:如果大多数商品对至少出现一次,使用三角矩阵(只存储上三角或下三角部分)是存储计数的有效方式。

计数表格:如果商品对出现的可能性很小,可以使用散列表来存储实际出现的商品对及其计数。

11.2.3 A-Priori 算法

该算法基于一个关键性质:频繁项集的所有非空子集也必须是频繁的。这一性质被称为频繁项集的单调性,它极大地减少了搜索空间,使得算法更加高效。

Apriori 算法流程

  1. 初始化
    • 读取数据集D,其中包含所有购物篮。
    • 设置支持度阈值s。
    • 初始化频繁项集集合F为空。
  2. 第一趟扫描
    • 计算所有单元集(单个商品的集合)的支持度。
    • 根据支持度阈值s,将支持度大于s的单元集加入F1 。
  3. 后续扫描(k=2到n)
    • 使用Fk-1(大小为k-1的频繁项集)生成候选集Ck(所有可能的k项集,但仅包含Fk-1中的项集作为子集)。
    • 扫描数据集D,计算Ck中每个候选项集的支持度。
    • 根据支持度阈值s,筛选出频繁项集Fk。
    • 如果Fk为空,则算法结束(因为不存在更大的频繁项集)。
  4. 输出
    • 输出所有频繁项集F1, F2, …, Fk,其中k是找到的最大频繁项集的大小。

例子:

**11.2.5.6.7 **A-Priori 算法中内存的优化

在Apriori算法中,第二趟扫描(n=2)是主存瓶颈,因为这时需要对候选的二元集进行计数,这通常比对更大的集合计数需要更多的空间。

为了减少第二趟扫描时候选二元集的数目,我们使用第一趟扫描时未使用的主存空间。

PCY算法:利用这块未使用的主存来完整地存放一个散列表。

PCY算法

  1. 第一趟扫描时的额外操作
    • 在为单个商品计数的同时,PCY算法还使用一个散列表来记录所有可能的商品对(i, j)的出现次数。这里的散列表的每个“桶”只存储一个计数值,用于记录对应商品对出现的次数。
    • 如果支持度阈值s较低,则可以使用更小的桶(如2字节)来进一步节省空间。
  2. 散列表到位图的转换
    • 在第一趟扫描结束后,将所有散列表中的桶转换成位图。如果某个桶的计数值大于或等于支持度阈值s,则对应位图中的位设为1,否则设为0。
    • 这一步骤极大地减少了空间占用,因为每个桶(原本可能占用2或4字节)现在只占用1位。
  3. 第二趟扫描时的优化
    • 在第二趟扫描时,PCY算法仅对满足以下两个条件的商品对(i, j)进行计数:
      • i和j都是频繁商品(即它们各自的支持度大于或等于s)。
      • 在位图中,商品对(i, j)对应的位为1,表示这对商品在第一趟扫描时至少以支持度s的频率出现过。
    • 这种方法显著减少了需要计数的候选二元集的数量,因为许多非频繁的或不太可能频繁的商品对在第一趟扫描后就被排除了。

11.3 发现近似的商品

11.3.1 相似度的 Jaccard 度量

** Jaccard 相似度** :一种衡量两个集合相似度的方法,定义为两个集合交集的大小除以并集的大小,![](https://img-blog.csdnimg.cn/img_convert/77d1acc56435a818bb2089535cde1b95.png) 。

Jaccard 相似度可以用来衡量商品、消费者、文档等的相似性。例如,两个商品如果经常一起出现在购物篮中,那么它们被认为是相似的。

11.3.2 Jaccard 相似度的应用

协同过滤
** 消费者推荐** :通过分析消费者购买的商品集合,可以发现购买行为相似的消费者。然后,可以向一个消费者推荐与他购买行为相似的其他消费者购买的商品。

商品推荐:相反地,也可以发现购买者集合相似的商品对,然后推荐那些经常一起被购买的商品。

相似文档
在处理大量文本数据时,如网页内容,可能需要识别内容相似的文档。这可以用于检测抄袭、避免搜索引擎返回重复内容等。

11.3.3 最小散列

最小散列(MinHash)是一种用于估计两个集合之间Jaccard相似度的技术,尤其适用于大数据集。它通过为每个集合生成一个较短的“签名”或“标签”来实现,这个签名能够反映集合的某些特征,使得我们可以通过比较这些签名来快速估计集合之间的相似度。

基本原理

  1. 定义全集和排列
    • 假设有一个全集 U={e1,e2,…,e__n},包含 n 个元素。
    • 随机选择 U 的一个或多个排列(permutation)。
  2. 计算最小散列值
    • 对于集合 S 和一个给定的排列 π,集合 S 的最小散列值 h__π(S) 是排列 π 中第一个属于集合 S 的元素。
    • 如果集合 S 不包含排列 π 中的任何元素,则可以选择一个默认值(如 ∞ 或一个非常大的数)。
  3. 生成标签
    • 选择多个(例如 m 个)不同的排列,并对每个排列计算集合 S 的最小散列值。
    • 集合 S 的标签(或签名)是由这些最小散列值组成的序列。

示例

假设全集 U={1,2,3,4,5},并且我们选择了三个排列:

  • π1=(1,2,3,4,5)
  • π2=(5,4,3,2,1)
  • π3=(3,5,1,4,2)

对于集合 S={2,3,4}:

  • π1 中,第一个属于 S 的元素是 2,因此 h__π1(S)=2。
  • π2 中,第一个属于 S 的元素是 4(注意顺序是反向的),因此 h__π2(S)=4。
  • π3 中,第一个属于 S 的元素是 3,因此 h__π3(S)=3。

所以,集合 S 的标签是 (2, 4, 3)。

11.4 局部敏感散列

最小散列标签方法并没能真正地解决的问题。尽管使用标签能大大加快估算两个集合的相似度的速度,但是我们需要去比较的集合对仍旧太多。

局部敏感散列(LSH)是一种用于大规模数据集中近似相似性搜索的算法。它的核心思想是将数据点映射到一个或多个哈希桶中,使得相似的数据点有更高的概率被映射到同一个桶中。这种方法可以减少需要直接比较的数据对的数量,从而提高效率。下面我将解释LSH的基本概念和例子,以帮助你理解。

基本原理

选择散列函数:选择一系列适合数据特性和相似度度量的散列函数。

构建散列表:使用这些散列函数将数据集中的元素映射到不同的哈希桶中。

候选集生成:对于查询元素,同样使用散列函数找到它可能落入的桶,并将这些桶中的元素作为候选集。

相似度验证:在候选集中进行更精确的相似度计算,找出真正相似的元素。

11.4.2 标签的局部敏感散列

假设我们有一个标签矩阵,其中每一列代表一个集合的标签(即该集合的最小散列标签),每一行代表一个散列函数的结果。我们将这个矩阵垂直地划分为多个带,每个带包含若干行(即散列函数的输出)。

对于每个带,我们定义一个散列函数,该函数将该带中所有行的值映射到一个很大的范围中的某个桶。这个散列函数的设计需要确保相似的标签有更高的概率被映射到同一个桶中。

对于每个桶中的标签对,我们进行详细的比较(例如,计算它们的Jaccard相似度),以确定它们是否真正相似。

11.4.3 最小散列法和局部敏感散列的结合

1. 首先MinHash技术为每个文档生成多个散列值。 2. 使用LSH,可以将这些散列值映射到多个“桶”中。 3. 对于每个候选对,可以通过比较它们的最小散列签名中相同位置的个数来估算它们的Jaccard相似度。 4. 对于估算相似度较高的候选对,可以通过直接比较它们的shingle集合来计算真实的Jaccard相似度。

11.5 大规模数据的聚簇

11.5.1 聚簇的应用

1. 聚类(聚簇)基础

聚类是将数据集中的点根据某种相似性度量分成若干组(簇)的过程。同一簇内的点相似度较高,而不同簇的点相似度较低。

相似度通常通过距离来定义。

2. 协同过滤

在商品推荐中,可以使用聚类技术将商品或用户聚集成簇,以便更好地理解它们之间的关系。

将购买行为相似的用户聚集成簇,或者将经常被一同购买的商品聚集成簇。

3. 基于主题的文档聚类

文档聚类是将文档根据内容主题进行分组的过程。

4. DNA序列聚类

DNA序列由特定的碱基对(C、G、A、T)组成,编辑距离是衡量两个序列之间差异的一种度量,它计算将一个序列转换为另一个序列所需的最少编辑操作次数。基于编辑距离的DNA序列聚类可以帮助识别出具有相似性的序列。

11.5.2 距离的定义

1. 基于范数的距离

在n维欧式空间中,距离通常定义为两点坐标差的平方和的平方根,即欧式距离(L₂-norm)。但更一般地,可以定义Lᵣ-norm距离,其中r是任意正实数。当r=1时,得到的是曼哈顿距离(L₁-norm),即所有坐标差值的绝对值之和。当r趋向于无穷大时,Lᵣ-norm距离趋近于所有维度上最大的坐标差值,这被称为L∞-norm距离。

2. Jaccard 距离

Jaccard 距离用于衡量两个集合之间的差异。它定义为1减去两个集合的Jaccard相似度,即两个集合交集大小与并集大小的比值的补数。

3. 余弦距离

余弦距离并不直接衡量两点之间的空间距离,而是衡量它们在n维空间中方向上的差异。具体来说,余弦距离是两个向量之间夹角的余弦值。

4. 编辑距离

编辑距离是衡量两个字符串之间差异的一种方式。它定义为将一个字符串转换为另一个字符串所需的最少单字符编辑(插入、删除或替换)次数

**11.5.3 凝聚式聚簇**

每个点各自为一个簇开始,不断地寻找“最近的”簇对进行合并,直至终止条件满足为止。

这种方法被称为凝聚式的或是层次化的聚簇。

  1. 最小距离:任何点对之间的最小距离,其中一个点来自簇C,另一个点来自簇D。
  2. 平均距离:所有点对距离的平均值,其中一个点来自簇C,另一个点来自簇D。
  3. 质心距离:簇C和簇D的质心之间的距离。质心是簇中所有点在每一维上坐标的平均值。

终止合并的条件

  1. 固定簇的个数:根据实际需求,设定一个簇的个数k,不断合并直到簇的数目达到k为止。
  2. 凝聚性条件:当两个簇合并后,如果合并后的簇不满足设定的凝聚性条件,则拒绝合并。

凝聚性指数

用来衡量簇内点的紧密程度

  1. 到质心的平均距离:簇中所有点到质心的距离的平均值。
  2. 簇的直径:簇中所有点对距离的最大值。
  3. 所有点对距离的平均值:簇中所有点对距离的平均值。

11.5.4 k-Means 算法

k-Means算法是一种广泛使用的聚类算法,它通过迭代的方式将数据点划分为k个簇,使得每个簇内的点相似度较高,而不同簇之间的点相似度较低。
  1. 选择初始簇中心:

从数据集中随机选择k个点作为初始的簇中心。或者,选择距离已选点尽可能远的点作为新的簇中心。

  1. 分配点到簇:

对于数据集中的每一个点,计算其与各个簇中心的距离。将每个点分配到距离其最近的簇中心所在的簇中。

  1. 更新簇中心:

对于每个簇,重新计算其所有点的均值(质心),并将该均值作为新的簇中心。

  1. 迭代优化:

重复步骤2和步骤3,直到满足某个终止条件(如簇中心的变化小于某个阈值,或者达到预设的迭代次数)。

11.5.5 大规模数据的 k-Means 方法

BFR(Batch and Focused Refinement)算法是一种用于处理大规模数据集聚类的算法,特别是在数据量大到无法一次性加载到内存中的情况下。该算法通过分批处理数据并维护簇的概括统计信息来减少内存的使用,并在多次迭代中逐步优化簇的质心和成员分配。

BFR算法

  1. 初始化:从数据集的一个子集中初始化k个簇的质心。
  2. 迭代处理
    • 将数据集分批加载到内存中。
    • 对于每一批数据,将其中的点分配到现有的簇中,或者创建新的临时簇(压缩集)来存放那些当前无法明确分配到任何簇中的点。
    • 更新每个簇(包括压缩集)的概括统计值(N, SUM_i, SUMSO_i)。
    • 将已分配到簇中的点从内存中移除,只保留概括统计值和尚未分配的点(保留集)。
  3. 聚焦细化:在多次迭代后,使用所有簇的概括统计值来计算新的质心,并可能合并一些压缩集到现有簇中。
  4. 重复迭代:直到满足某个终止条件(如质心变化小于阈值、达到最大迭代次数等)。

11.5.6 内存中满载点后的处理过程

内存中满载点后,BFR采用一系列策略来有效地处理内存中的数据。

1. 点的分配与丢弃集更新

对于接近某个簇质心的所有点,算法会将其加入到那个簇中,并将这些点归入丢弃集。同时,更新该簇的统计信息。

2. 压缩集与保留集的最终分配

如果这是最后一次往内存中加载数据,那么算法会将所有压缩集中的组和保留集中的点分配到距离最近的簇中。

3. 内存聚簇算法的应用

如果不是最后一次往内存中加载数据,算法会使用任意主存聚簇算法对这次读取中剩余的点以及当前保留集中的点进行聚簇。

4. 更新保留集与压缩集

聚簇完成后,算法会重新评估各个簇的大小。那些大小为1的簇(即只包含一个点的簇)中的点会构成新的保留集。

而大小超过1的簇则会被加入到压缩集中,并用它们的概括统计值代替原始数据点。

5. 压缩集的合并

算法可能需要合并一些压缩集中的组以进一步减少内存使用量或提高聚簇的质量。

第12章 数据库系统与互联网

12.1搜索引擎体系结构

![](https://img-blog.csdnimg.cn/img_convert/4f67db239e200b526fb5e4ab4d47507e.png)

1. 爬虫(Crawler)

爬虫是搜索引擎的“侦察兵”,负责在互联网上自动抓取(或称为“爬取”)网页内容。它们从一组初始的网页URL开始,然后遍历这些网页上的链接,继续访问并抓取这些链接指向的网页,如此循环往复,直至覆盖尽可能多的网页。

它们将抓取到的网页数据存储到网页库中,供后续处理和索引。

2. 网页库(Page Repository)

网页库是一个存储爬虫抓取到的所有网页内容的数据库。这个数据库是搜索引擎后续处理和索引的基础。

3. 索引器(Indexer)

索引器负责对网页库中的网页内容进行索引。最常用的索引方法是倒排索引(Inverted Index),它为每个词建立一个索引项,索引项中包含该词出现的所有网页的列表。此外,索引中还可能包含词的附加信息,如词在网页中的位置、出现频率、是否出现在标题中等,这些信息有助于后续的查询处理和排序。

4. 查询处理器(Query Processor)

当用户输入查询词并提交查询时,查询处理器负责接收并解析这些查询词。它使用解析后的查询词与索引进行交互,以找出包含这些词的网页。

5. 排序器(Ranker)

排序器根据一定的算法对查询处理器找出的网页进行排序。排序的依据可能包括网页的相关性、权威性、时效性等多个因素。排序的目的是将最符合用户查询需求的网页排在前面,以便用户能够更快地找到所需信息。

6. 用户界面(User Interface)

功能:用户界面是用户与搜索引擎交互的窗口。它负责接收用户的查询输入,并显示查询结果。通常,查询结果会以列表的形式呈现给用户,列表中的每个条目都代表一个网页的摘要信息,包括网页的标题、URL和一小段描述文本等。

12.1.2 Web 爬虫

** 终止条件**
  • 网页数量限制:可以设定总的网页抓取数量或针对每个站点的网页数量上限,防止爬虫无休止地运行。
  • 深度限制:设置爬取深度,初始页面深度为1,随着爬取深入,增加深度值,到达预设深度后停止进一步探索。

网页库管理

  • 避免重复抓取:在将新URL加入待抓取集合S前,检查其是否已存在于S或已抓取网页库R中。
  • 处理重复内容:利用散列函数生成网页的唯一标识(如64位信号),通过散列表快速判断网页是否已被抓取。对于几乎相同的网页,可以使用最小散列签名和局部敏感哈希技术提高识别效率。

选择下一个网页

  • 广度优先搜索:从起始点开始,按照广度优先顺序选择下一个抓取目标,有助于优先访问重要网页。
  • 基于重要性选择:使用PageRank或其他指标评估网页的重要性,优先抓取评分较高的网页。

提高爬虫速度

  • 多进程/多线程:允许多个进程或线程并行工作,但需要对共享资源(如待抓取URL集合S)进行同步控制,避免冲突。
  • 分散负载:可以通过将进程分配给不同站点或使用散列技术将URL集分布到多个桶中,减少对单一资源的竞争,从而提高效

12.1.3 搜索引擎中的查询处理

** 搜索引擎查询与SQL查询的区别**
  • 关键词查询:搜索引擎查询通常基于一组关键词,搜索引擎的任务是找到包含这些关键词(全部或部分)的网页,并根据一定规则对结果进行排序。
  • 布尔组合:搜索引擎支持关键词之间的逻辑组合,比如AND(并且)、OR(或者)和NOT(非)。例如,查找包含“data”或“base”的网页。
  • 邻近搜索:某些查询要求关键词在文档中有特定的排列或接近度,比如两个词之间最多相隔5个词。这类查询需要更复杂的索引结构来支持。

倒排索引的作用

  • 索引构建:搜索引擎抓取网页后,索引管理器会为Web上的每个词创建一个倒排索引。倒排索引记录了每个词出现在哪些文档中,以及出现的位置。
  • 索引范围广泛:倒排索引不仅涵盖标准词汇,还包括错误拼写、代码、缩写、名称等,这大大增加了索引的复杂性和规模。

查询处理过程

  • 快速定位:查询处理的第一步是利用倒排索引来确定哪些文档包含了查询关键词。为了保持响应时间在用户可接受的范围内(通常不超过1秒),搜索引擎需要尽量减少磁盘I/O操作。
  • 位向量技术:对于频繁出现的词,搜索引擎通常不会为每个词列出所有相关的文档,而是使用位向量来表示。位向量中的每一位代表一个文档,位向量的值表示该词是否出现在对应的文档中。
  • 逻辑运算:通过对位向量执行逻辑“与”、“或”等操作,可以高效地找出同时包含多个关键词或任一关键词的文档集合。
  • 内存优化:为了加快查询速度,搜索引擎会在内存中缓存尽可能多的位向量,避免频繁的磁盘访问。此外,通过分布式计算的方式,可以将查询处理任务分发到多台机器上,每台机器处理一部分文档的位向量,从而显著提升查询效率。

12.1.4 对网页进行排名

对满足查询条件的网页进行排名。

PageRank

具体来说,如果一个网页被很多其他高质量的网页链接,那么这个网页的PageRank值就越高。PageRank算法考虑了链接的数量和链接来源的质量,而不是单纯地基于链接数量。

其他相关性衡量标准

  1. 查询关键字的全面出现
    • 如果一个网页包含了查询中的所有关键字,那么这个网页的相关性评分通常会高于只包含部分关键字的网页。
  2. 关键字在网页中的位置
    • 关键字出现在网页的重要位置(如标题、H1标签、元描述等)比出现在正文段落中更能增强网页的相关性。
  3. 关键字的紧密度
    • 当查询包含多个关键字时,这些关键字在网页中出现的距离越近,网页的相关性评分就越高。
  4. 关键字在锚文本中的出现
    • 锚文本是指超链接中可见的文本部分。如果查询的关键字出现在链接到目标网页的锚文本中,或者出现在链接附近的文本中,这会增加目标网页的相关性评分。

12.2 用于识别重要网页的 PageRank

** 1. 链接的重要性**
  • 链接作为投票:在Web上,一个网页对另一个网页的链接可以被视为一种投票。如果一个网页A链接到网页B,这意味着网页A认为网页B是有价值的。
  • 投票权重:并不是所有的链接都具有相同的权重。一个网页的PageRank值越高,它对其他网页的投票就越有价值。

2. 随机漫步者模型

  • 随机漫步者:假设有一个随机漫步者在Web上随机点击链接。每一步,漫步者会从当前网页p随机选择一个链接,跳转到下一个网页。
  • 访问概率:每个网页的PageRank值可以理解为随机漫步者访问该网页的概率。这个概率反映了网页的重要性。

转移矩阵的性质

  1. 随机矩阵
    • 如果每一个网页至少有一个链出(即每个网页至少有一个出链),则转移矩阵 ( M ) 是一个(左)随机矩阵。这意味着矩阵中的所有元素都是非负数,并且每一列的元素之和为1。
    • 这是因为每个网页 ( j ) 的出链数 ( r ) 大于等于1,所以每一列的元素之和为1。
  2. 半随机矩阵
    • 如果有的网页没有链出(即某些网页的出链数为0),则这些网页对应的列全为0值。此时,转移矩阵 ( M ) 称为半随机矩阵,因为某些列的元素之和小于1(甚至可能为0)。

举例

假设我们有三个网页 A、B 和 C,它们之间的链接关系如下:

  • A -> B
  • B -> C
  • C -> A 和 C -> B

我们可以构造一个 转移矩阵 ( M ):

解释如下:

悬挂节点

  • 如果某个网页没有出链,对应的列全为0,这会导致随机漫步者在这个网页上“悬挂”(即无法继续移动)。为了解决这个问题,可以引入一个全局随机跳转概率 ( d )(通常取0.85),并假设随机漫步者有 ( 1 - d ) 的概率随机跳转到任何一个网页。

12.3 特定主题的 PageRank

“远距离移动”集

+ **远距离移动集** (teleport set)是一组特定的网页,随机漫步者在进行“远距离移动”时,可以选择这些网页,而不是随机选择Web上的任何一个网页。

目的

  • 解决死角和爬虫陷阱:通过允许随机漫步者“远距离移动”到特定的网页,可以避免随机漫步者被困在死角或爬虫陷阱中。
  • 提升特定网页的PageRank:通过将“远距离移动”的概率集中到某些特定的网页,可以提高这些网页及其链接到的网页的PageRank值。

计算主题相关的PageRank

+ 通用的PageRank算法将所有网页视为平等,可能导致不相关的网页出现在搜索结果中。例如,查询“击球手”时,通用PageRank可能会返回与纸托蛋糕食谱相关的网页,而不仅仅是与棒球相关的网页。

方法

  • 选择远距离移动集:选择与特定主题相关的网页作为远距离移动集,从而提高这些网页及其链接到的网页的PageRank值。

打击链接作弊

+ 许多不道德的网站通过制造大量的链接来提高自己的PageRank,这种行为称为“链接作弊”。

方法

  • TrustRank:TrustRank是一种特殊的PageRank,通过选择可信任的网页作为远距离移动集,来降低作弊网页的PageRank。

步骤

  1. 选择可信任网页集
    • 人工评估:人工检查网页并评估其可信度。
    • 初始选择:选择那些通常被认为是可信任的网页,例如大学主页。
  2. 计算TrustRank
    • 正常PageRank:计算所有网页的正常PageRank。
    • TrustRank:使用可信任网页集作为远距离移动集,计算所有网页的TrustRank。
  3. 计算作弊率
    • Negative TrustRank:计算每个网页的PageRank和TrustRank之差。
    • 作弊率:用Negative TrustRank与PageRank的比值作为网页的作弊率。
  4. 检测和处理作弊网页
    • 高作弊率网页:如果一个网站中有许多网页都有很高的作弊率,这个网站可能是作弊者,可以将其从搜索引擎的数据库中删除。

12.4 数据流

12.4.1 数据流管理系统

![](https://img-blog.csdnimg.cn/img_convert/587d7e1cc32a75a0021f0aa376f35511.png)

对数据流的查询可以分为以下两类:

  1. 一般即席(Ad-hoc)查询:这类查询是临时性的,用户可以根据需要随时发起。例如,查询过去1小时内某个地点的平均辐射能级。
  2. 常驻查询(Standing Query):这类查询是由系统存储并在输入的数据流上持续执行的。例如,监控来自任何数据流的数据是否超过某个阈值。

12.4.3 数据流数据模型

为了便于分析基于数据流的算法,我们给出一个数据模型。首先,假设数据流具有以下特征:
  1. 元组序列:每个数据流包含一个元组序列。这些元组类似于关系数据库中的元组,具有固定的模式(即属性列表)。与关系数据库不同的是,数据流中的元组序列可能是无限的。
  2. 到达时间:每个元组都有一个到达时间,此时数据流管理系统(DSMS)能够对其进行处理。DSMS可以对元组进行处理,如将其存储在工作存储区或永久存储区,或将元组从存储区删除。在存储元组之前,可以进行一些简单的处理。

对于任何数据流,我们可以定义一个滑动窗口(或简称窗口),即最近到达的元组集合。窗口可以是基于时间的(time-based)或基于元组的(tuple-based):

  • 基于时间的窗口:给定一个常量 r,窗口中仅包含到达时间介于当前时间 t和 tr 范围内的元组。
  • 基于元组的窗口:给定一个固定值 n,窗口中仅包含最近到达的 nn 个元组。

我们用符号 S[W]来描述数据流 S 上的窗口,其中 W描述了窗口的种类,可以是:

  1. Rows n:仅包含最近到达的 n个元组。
  2. Range r:仅包含过去 r时间范围内的元组。

12.4.4 数据流转换为关系

可以针对数据流将SQL语句进行扩展,像处理关系那样处理窗口表达式。下面的例子描述了一个扩展后的SQL语句。

假设对于每个传感器,我们想找出最近1小时内到达DSMS的最高温度。这里我们需要采用基于时间的窗口,并像查询传统关系那样查询它。查询表示如下:

SELECT sensID, MAX(temp)
FROM Sensors[Range 1 Hour]
GROUP BY sensID;

12.4.5 关系转换为数据流

当我们做常驻查询时,所得的关系会频繁地被更新。如果像维护物化视图那样维护这些关系,会导致大量的插入和删除操作。另一个可行的办法是将查询所得的关系转换为数据流,从而可以使我们像对待其他数据流那样进行操作。例如,如果某个时刻我们对关系的值感兴趣,可以做即席查询来得到查询结果。

假设 ( R ) 是一个关系,我们用 Istream(R) 来表示插入到 ( R ) 中的元组所组成的数据流。当元组被插入到关系时,也会加入到这个数据流中。

Dstream(R) 来表示从 ( R ) 中被删除的元组所组成的数据流,当元组从关系中删除时,就会加入到这一数据流中。对关系中元组的更新可以通过同时对元组进行插入和删除操作来完成。

假设我们有一个关系 R,其中包含以下元组:

IDValue
110
220

如果我们将元组 (3, 30) 插入到 R 中,那么 Istream(R) 将包含:

IDValue
330

如果我们将元组 (1, 10)R 中删除,那么 Dstream(R) 将包含:

IDValue
110

通过这种方式,我们可以将关系的变化转换为数据流,从而可以在数据流管理系统中进行进一步的处理和分析。这种方法不仅简化了数据管理,还提高了系统的灵活性和效率。

12.5 数据流挖掘

本节主要介绍如何更简洁地表示窗口中的内容,而无需逐一列出窗口中的每个元组。

12.5.1 动机

为了获得有效的响应,我们希望将数据全部存储在主存中。但对于几个长度多达10亿的窗口,或上千个长度为100万的口,没有足够大的内存来应对。因此,我们需要压缩窗口中的数据。但如果压缩了窗口,即使对于一些简单的查询也无法回答。

如果任何时刻都想得到精确的和,就不能对一个滑动窗口的和进行压缩。但如果我们能够接受和的近似值,就有许多可选方法。这里介绍一个非常简单的方法。

我们将数据流元素进行分组,每组包括100个元素。即数据流返回第1组100个元素,再返回第2组100个元素,依此类推,每一组用其中的元素和来表示。对于理论上的每100个整数,窗口实际上只存了一个数。

假设某时刻有一个整数到达窗口,这个整数开始了一个新组,我们把这个整数本身作为这一组的和,现在我们只能通过估计来得到窗口的和。我们将所记录的最早一组的和的99%,加上窗口中所记录的其他组的和,来估计窗口中所有整数的和。

12.5.2 统计二进制位数

在数据流挖掘中,特别是处理二进制数据流时,一个常见的需求是估计滑动窗口内1的数量。为了高效处理这种查询,同时降低存储需求和查询时间,可以使用一种称为“桶”的数据结构来近似计算1的数量。这种方法不仅减少了所需的存储空间,还能在合理的误差范围内快速回答查询。

桶的定义和性质

桶的大小:每个桶的大小是2的幂(如1, 2, 4, 8, …),这意味着桶可以表示不同数量的1。

时间规则:从最新的位(时间戳最大)开始,桶的大小是不降的。即,不会有一个大小为4的桶后面紧跟着一个大小为2的桶。

数量规则:对于每个大小的桶(m=1, 2, 4, …),最多有两个这样的桶存在。这是为了确保桶的数量不会过多增长,从而控制存储空间。

桶的表示:每个桶(m, t)由两部分组成:桶的大小m(以2的幂次表示,因此实际存储的是logm)和桶中最新的1到达的时间t(通过对N取余来限制时间范围,因此用logN位表示)。

桶的存储和查询

存储:由于桶的大小和数量都是对数的,因此整个窗口的状态可以用O(logN)个二进制位来表示,远小于直接存储N个二进制位所需的O(N)空间。

查询:要回答“最近k位中有多少个1”的查询,算法会找到第一个其最后一个1的到达时间在k范围内的桶B。然后,它会将所有在k范围内的完整桶的1的数量加起来,并对桶B中的1的数量进行估计(通常是桶大小的一半)。

桶的维护

新位到达:当一个新的二进制位到达时,如果是0,则不影响桶的结构;如果是1,则可能需要创建一个新的桶。如果同一大小的桶数量超过两个,则需要合并桶以维持规则。

桶的合并:合并过程会递归地将相邻的、大小相同的桶合并成一个更大的桶,直到满足规则为止。合并时,新桶的时间戳是原来两个桶中较新的那个。

误差分析

误差来源:误差主要来自于对部分在查询范围内的桶的1的数量的估计。通常,我们使用桶大小的一半来估计这部分数量。

误差范围:误差率的上限是50%,但这在实际应用中通常是可以接受的,因为这是一种近似方法。重要的是,通过调整桶的大小和数量,我们可以控制误差的大小,以满足不同的应用需求。

举例:

开始时,我们没有任何二进制位到达,因此没有桶。随着二进制位的到达,我们开始创建桶。

二进制位到达

1. ** 时间1** : 二进制位 ` 1` 到达。我们创建一个大小为1的桶 ` (1, 1)` ,表示这个桶有1个1,且这个1是在时间1到达的。 2. ** 时间2** : 二进制位 ` 0` 到达。这个位不影响桶的结构,因此无需更改。 3. ** 时间3** : 二进制位 ` 1` 到达。我们再次创建一个大小为1的桶 ` (1, 3)` 。此时,我们有两个大小为1的桶,这符合规则(每种大小的桶最多有两个)。 4. ** 时间4** : 二进制位 ` 1` 到达。由于此时我们已经有两个大小为1的桶,我们需要合并它们。我们创建一个新的桶 ` (2, 3)` ,表示这个桶有2个1,且这两个1中最晚到达的是在时间3。 5. ** 继续这个过程** ,直到我们填充了整个滑动窗口。在这个过程中,我们会不断地创建新桶、合并桶,并丢弃那些超出窗口范围的桶。

查询处理

假设在某个时间点,我们想要回答“最近k=8位中有多少个1”的查询。
  1. 查找最早的桶:我们首先找到桶中最后一个1的到达时间在查询范围(最近8位)内的最早的那个桶。假设这个桶是 <font style="color:#000000;background-color:rgb(253, 253, 254);">(4, 5)</font>,表示它有4个1,且这些1中最晚到达的是在时间5。但注意,这个桶可能只有部分在查询范围内。
  2. 计算完整桶中的1:我们累加所有完全在查询范围内的桶中的1的数量。假设除了 <font style="color:#000000;background-color:rgb(253, 253, 254);">(4, 5)</font> 之外,还有两个完整桶 <font style="color:#000000;background-color:rgb(253, 253, 254);">(2, 2)</font><font style="color:#000000;background-color:rgb(253, 253, 254);">(1, 1)</font>,则这些桶中的1的总数为 2 + 1 + 1 = 4。
  3. 估计部分桶中的1:对于部分在查询范围内的桶(如 <font style="color:#000000;background-color:rgb(253, 253, 254);">(4, 5)</font>),我们使用桶大小的一半来估计在查询范围内的1的数量。因此,对于 <font style="color:#000000;background-color:rgb(253, 253, 254);">(4, 5)</font> 桶,我们估计有 4/2 = 2 个1在查询范围内。
  4. 汇总结果:将完整桶和部分桶中的1的数量相加,得到查询结果。在这个例子中,查询结果是 4 + 2 = 6。

12.5.3 统计不同元素的个数

如何统计一个数据流中在特定窗口(比如一个月)内不同元素的个数。这里的“不同元素”可以是不同的用户登录名、不同的单词等。直接存储并计算整个窗口的所有元素是不现实的,因为数据量可能非常大,会消耗大量存储空间和计算资源。因此,我们需要一种近似的方法来估计不同元素的数量。

提出的方法

  1. 散列函数:对于每个到达的元素v,我们使用一个散列函数h将其映射为一个整数。这个整数在二进制表示下可能有多个位。
  2. 尾部零的个数:对于每个散列值h(v),我们计算其二进制表示中尾部连续零的个数,记为r。
  3. 更新R:我们维护一个变量R,初始值为0。每当一个新的元素到达,并且其散列值的尾部零的个数r大于R时,我们就更新R为r。
  4. 估计值:最后,我们使用2^R作为不同元素数量的估计值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值