# 物理存储数据

# 1. 存储设备对比

  • 比较了硬盘(HDD)和内存(RAM)的特点:
    • 硬盘:容量大、价格低、永久存储,但访问速度较慢。
    • 内存:速度快、支持随机访问,但存储成本高且数据易失​。

# 2. 数据块和缓冲管理

  • 数据在硬盘上按 “块”(blocks)组织,每个块为固定大小(通常为 4KB 或 8KB),块是数据传输的基本单位。
  • 缓冲管理器负责将数据块加载到内存,并在内存满时使用替换算法决定保留哪些数据块。常用的替换算法包括 **LRU(最近最少使用) MRU(最近最多使用)**​。

# 2.1 数据块(Blocks)

  • 数据库中的数据通常存储在磁盘上,并按 “块”(block)为单位进行管理。数据块是操作系统在磁盘和内存之间传输数据的基本单位。
  • 块大小一般为 4KB 或 8KB,具体取决于操作系统和硬件配置。块的大小会影响 I/O 操作的效率:较大的块可以减少读取大文件时的 I/O 次数,但可能会增加小文件的空间浪费​。
  • 块因素(Blocking Factor):这是块大小与记录大小的比率,决定了一个数据块中可以容纳多少条记录。如果记录较大,可能需要多个块来存储一条记录,这种情况称为 “记录跨块”(record spanning)。跨块存储会增加 I/O 复杂度,通常需要避免​。

# 2.2 缓冲区(Buffer)和缓冲管理器(Buffer Manager)

  • 缓冲区是数据库系统在内存中保留的一块区域,用于暂存从磁盘加载的块。由于内存的访问速度比磁盘快得多,将数据块缓存在内存中可以显著提升查询速度。
  • 缓冲管理器负责管理这些缓冲区块,主要任务包括:
    • 块的加载和释放:根据需求从磁盘加载块到内存缓冲区,释放不再使用的块。
    • 块替换策略:当内存缓冲区已满且需要加载新块时,选择要被替换的块。

# 2.3 块替换策略

  • 数据库系统通常使用以下常见的替换算法来优化内存利用率:
    • LRU(Least Recently Used,最近最少使用):优先替换最久未使用的块,适用于访问数据呈局部性特征的情况。
    • MRU(Most Recently Used,最近最多使用):优先替换最近使用的块,适用于某些需要频繁访问相同数据块的情况,如嵌套循环连接操作​。
  • 这些替换策略帮助缓冲管理器在内存中保留最可能被再次访问的块,从而减少频繁的 I/O 操作,提高查询性能。

# 2.4 缓冲区示例

  • 通过一个连接操作的示例说明了如何选择替换策略:
    • 假设执行的是一个嵌套循环连接,内表的块可能需要频繁访问,因此在这种情况下使用 MRU 策略会更有效,因为最近访问的块很快会再次被访问。
    • 通过合理的缓冲区管理和替换策略,可以显著减少 I/O 操作次数,从而提高查询效率​。

# 2.5 缓冲管理的重要性

  • 由于数据库通常远大于内存容量,无法将所有数据同时存储在内存中,因此需要有效的缓冲管理。
  • 缓冲管理的目标是通过智能的块加载和替换策略,将最常用的数据保留在内存中,以最小化磁盘访问次数,从而优化数据库系统的整体性能。

# 3. 存储示例和填充因子(Fill Factor)

  • 举例说明了数据表中记录如何分布在数据块中。假设每条记录 200 字节,每页可容纳 19 条记录,因此需要 105,264 个数据页来存储共 2,000,000 条记录​。
  • 填充因子(Fill Factor)用于决定每个数据页中预留多少空间,以便在新数据插入时无需频繁分裂页面。

# 3.1 存储示例

  • 数据库表以固定大小的页面(page)形式存储,每个页面通常为 4KB。页面用于存储表中的多条记录。
  • 示例假设:
    • Relation 包含 2,000,000 条记录,每条记录大小为 200 字节。
    • 每个页面大小为 4KB,其中 250 字节用于页面头部和记录指针数组,其余 3,846 字节用于实际数据存储​。
  • 每页存储的记录数
    • 每条记录为 200 字节,因此每页可以容纳的记录数为: 记录数 =⌊3846 字节 200 字节 / 记录⌋=19 条记录 \text {记录数} = \left\lfloor \frac {3846 \text { 字节}}{200 \text { 字节 / 记录}} \right\rfloor = 19 \text { 条记录} 记录数 =⌊200 字节 / 记录 3846 字节​⌋=19 条记录
    • 因此,总共需要约 105,264 个页面来存储全部记录​。

# 3.2 填充因子(Fill Factor)

  • 定义:填充因子决定了每个页面用于存储数据的空间百分比。设定较低的填充因子会在页面中预留一定的空闲空间,便于后续插入新数据。
  • 作用:通过保留空闲空间,填充因子减少了页面分裂的频率,有助于提高索引的维护效率。通常数据库在创建索引或存储页面时可以设置填充因子的值,如 80% 或 75%。
  • 计算示例
    • 若填充因子设置为 75%,则每页仅填充 3/4 的数据: 19 条记录 ×75%=14 条记录 / 页(取整后)19 \text {条记录} \times 75% = 14 \text { 条记录 / 页(取整后)} 19 条记录 ×75%=14 条记录 / 页(取整后)
    • 在这种情况下,需要的页面数增加到 142,858 页,以满足较低填充因子下的存储需求,这也会带来额外的存储开销​。

# 3.3 填充因子的影响

  • 性能优化:较低的填充因子提高了插入新数据的灵活性,减少了频繁页面分裂的开销,适合需要大量插入操作的场景。
  • 空间开销:虽然较低的填充因子带来额外的存储开销,但它能显著减少页面分裂的次数,从而提高系统的整体性能。
  • 示例中,设置填充因子为 75% 带来了 47% 的额外存储开销。这种设计通常用于索引页面,以保证索引更新时的效率​。

# 4 典型文件组织方式

  • 堆文件(Heap Files):无序存储,适用于全表扫描,插入和删除开销较小。
  • 排序文件(Sorted Files):按键值排序,适合二分查找,但插入和更新成本较高。
  • 索引文件(Index Files):利用索引(如 B + 树或哈希索引)来提升数据访问速度​。

# 4.1 无序文件(Heap Files)

  • 定义:无序文件将记录以随机顺序存储在可用空间中,不对数据进行任何排序。
  • 特点
    • 优点:插入和删除操作快速简单,因为无需重新排列数据。
    • 缺点:在没有索引的情况下,查询需要扫描整个表(即线性扫描),查询效率较低。
  • 适用场景:适合需要频繁插入和删除数据的表,特别是需要全表扫描的场景,如日志记录和历史数据存储​。

# 4.2 排序文件(Sorted Files)

  • 定义:排序文件按特定的搜索键(search key)对记录进行排序存储。
  • 特点
    • 优点:对等值查询和范围查询非常高效,支持二分查找,从而减少了 I/O 操作。
    • 缺点:插入和删除操作代价较高,因为插入新记录或删除记录后需要保持数据的排序。
  • 适用场景:适合数据更新较少、查询频繁的场景,例如数据仓库或查询量大的报表数据​。

# 4.3 索引文件(Index Files)

  • 定义:索引文件在特定字段上建立数据结构(如 B + 树或哈希表),用于加速数据检索。
  • 特点
    • 优点:索引能够显著提高查询效率,尤其适用于等值查询和范围查询(如 B + 树索引)。此外,索引通常存储在独立的页面中,并与数据页分开。
    • 缺点:需要额外的存储空间来存放索引页;每次数据插入、删除或更新时,索引页也需要更新,带来额外的 I/O 开销。
  • 典型索引类型
    • 哈希索引:适合等值查询,不支持范围查询。
    • B + 树索引:既支持等值查询,也支持范围查询,是关系型数据库中常用的索引结构​。

# 4.4 文件组织方式的比较

  • 无序文件在插入和删除操作中速度较快,但查询效率较低。
  • 排序文件在范围查询和等值查询上较高效,但插入和删除操作较慢。
  • 索引文件通过额外的索引结构在查询上具有显著优势,但维护索引需要额外的 I/O 开销和存储空间。

# 4.5 适用性总结

  • 无序文件适合高频插入和删除且不要求快速查询的场景。
  • 排序文件适合更新较少且需要高效范围查询的场景。
  • 索引文件是最常用的方式,通过索引结构显著提升查询效率,尤其适合查询频繁的业务表​。

# 如何从 DBMS 中检索记录

# 1. 访问路径(Access Paths)

  • 定义:访问路径是一种用于检索记录的方法,包括用于检索和存储表中记录的数据结构(如索引)和算法。
  • 三种访问方法
    • 线性扫描:适用于无序(Heap)文件,对所有记录逐一检查。
    • 二分查找:适用于排序文件,I/O 成本较低,可快速找到目标记录。
    • 索引扫描:使用索引可以直接定位记录,效率高​。

# 2. 物理数据独立性(Physical Data Independence)

  • 定义:选择访问路径的方式不会影响 SQL 语句的语义,但可能对执行时间有很大影响。
  • 物理数据独立性确保了查询语句与底层存储结构的独立性​。

# 3. 无序文件的访问方法

  • 特点:无序文件的记录没有特定的顺序。
  • 访问方法:通常通过线性扫描实现,每次检查一个页面中的所有记录。
  • 性能:平均情况下需要读取一半页面,最坏情况下需要读取整个文件​。

# 4. 线性扫描的 I/O 成本示例

  • 假设表有 140,351 个页面,用于查询一个唯一的 tuplekey
  • 如果 tuplekey唯一的,一旦找到匹配记录可以立刻终止。
  • 平均情况下,需要检查约一半的页面,因为匹配记录可能位于任意位置。I/O 成本大约为: 140,3512≈70,176 次 I/O\frac {140,351}{2} \approx 70,176 \text { 次 I/O} 2140,351​≈70,176 次 I/O
  • 最坏情况:如果没有匹配记录或 tuplekey 不是唯一的,则需要扫描所有 140,351 个页面,即需要 140,351 次 I/O 操作。
  • 例如,执行查询 SELECT * FROM Relation WHERE attribute1 BETWEEN 100 AND 119;
  • 对于范围查询,由于无序文件中记录是随机存放的,因此无法提前结束扫描,需要检查每个页面中的每条记录。
  • 这类查询通常会导致全表扫描,因此 I/O 成本为 140,351 次 I/O​。

# 5. 排序文件的访问方法

  • 排序文件:记录按某属性排序存储,适合频繁的等值和范围查询。
  • 二分查找:对于包含 140,351 个页面的表,最坏情况下 I/O 成本为 log⁡2140,351≈18\log_2 140,351 \approx 18log2​140,351≈18 次 I/O​。
    <二分查找图 -- 回家再上传>

# 6. 索引(Indexing)

  • 定义:索引是一种数据结构,用于将搜索键值映射到记录集合。
  • 常见索引类型
    • 哈希索引:适合等值查询,但不支持范围查询。
    • B + 树索引:支持等值和范围查询,适合关系型数据库。
  • 缺点:索引需要额外的存储空间,并在表数据更新时带来维护开销,但总体上索引带来的查询性能提升大于这些缺点​。

# B + 树

# 1. B + 树的定义

  • B + 树是一种自平衡的树形结构索引,用于在磁盘上组织和管理数据。
  • 与 B 树不同,B + 树将所有数据都存储在叶子节点中,而内部节点仅用于存储键值和指针,以引导查询过程。
  • 叶子节点按键值顺序连接,形成一个链表,支持高效的范围查询。

# 2. B + 树的结构特点

  • 内部节点(Internal Nodes):存储搜索键和指针,用于引导搜索过程,但不存储实际数据。
  • 叶子节点(Leaf Nodes):存储所有数据条目,且按顺序链接,支持顺序访问。
  • 树的高度:B + 树的所有叶子节点在同一层,路径长度一致,使得树是平衡的。因此,在 B + 树中查找任意记录所需的 I/O 次数相同。

# 3. B + 树的构建过程

  • 索引条目:在每个记录页面生成一个索引条目,并将这些索引条目组织成 B + 树的内部结构。
  • 层次结构:构建 B + 树的多层结构,从叶子层到根节点逐层递归构建。
  • 填充因子(Fill Factor):通常设置在 75% 左右,避免叶子节点过满,提高插入、删除的效率。

# 4. B + 树的查找过程

  • 等值查询(Equality Search)
    1. 从根节点开始,根据键值逐层向下查找,直到找到目标叶子节点。
    2. 在叶子节点中查找目标记录。
  • 范围查询(Range Search)
    1. 通过等值查询找到范围的起始位置。
    2. 然后按顺序访问相邻的叶子节点,直到范围结束。

# 5. B + 树的优点

  • 高效的等值查询和范围查询:B + 树结构保证了所有叶子节点的有序性,支持快速定位范围的起始位置。
  • 磁盘 I/O 效率高:B + 树通过多层结构减少了每次查询所需的 I/O 次数。在大数据集上,B + 树的层数较少,通常只需 3-4 次 I/O 即可找到目标记录。
  • 支持动态更新:插入和删除操作可以在 B + 树中高效执行,且不会影响其平衡性。

# 6. B + 树的劣势

  • 额外的存储开销:B + 树需要存储内部节点和叶子节点的指针,会占用额外的存储空间。
  • 维护成本:每当数据插入、删除时,B + 树需要保持平衡和顺序,带来一定的维护开销。

# 7. B + 树在数据库中的应用

  • B + 树常用于关系型数据库中的主键索引、唯一索引和范围索引。
  • 它在查询优化中发挥关键作用,特别是在大规模数据集的等值查询和范围查询中,可以显著减少 I/O 操作次数。

# 8. 举例:

# 1. 构建逻辑有序数据文件

  • 假设表 Relation 包含 140,351 个页面,每个页面中的记录按 tuplekey 逻辑排序。
  • 页面内的记录按键值排序,但页面在磁盘上的存储位置可能并不连续。这种组织方式称为逻辑有序文件​。

# 2. 为每个记录页面创建索引条目

  • 为数据文件中的每个页面创建一个索引条目,每个条目包含搜索键( tuplekey )和一个指向数据页面的指针 rowid
  • 每个索引条目占用 8 字节(4 字节的键和 4 字节的指针),可以在 4KB 页面中存储最多 480 个索引条目​。

# 3. 将索引条目装入页面

  • 假设页面的填充因子(即平均占用率)为 75%,则每个索引页面平均包含 360 个索引条目。
  • 这样,索引层次中将需要 140,351 / 360 ≈ 390 个索引页面​。

# 4. 构建 B + 树索引

  • 索引的下一层包含 390 个索引条目,即需要 2 个页面来存储这些条目。
  • 根层包含 2 个索引条目,因此只需 1 个页面。
  • 因此,B + 树索引的总页面数为 390 + 2 + 1 = 393 页,相对于数据表的大小仅增加了 0.2% 的存储​。

# 5. B + 树索引中的查找过程

  • 例如,执行查询 SELECT * FROM Relation WHERE tuplekey=715; 时,可以按以下步骤查找:
    1. 加载根页面到内存(1 次 I/O)。
    2. 找到下一级索引页面的位置并加载(1 次 I/O)。
    3. 重复直到找到叶子节点页面。
    4. 加载包含目标记录的叶子页面,并检查每条记录。
  • 总共约需 4 次 I/O,而无序文件中线性扫描的 I/O 成本可达 70,176 次​。