DBMS/数据库(二)存储管理

390 阅读14分钟

存储管理

存储管理简述

通常大多数 DBMS 使用已有的文件系统存储数据而非块或原始文件。

存储管理是什么

  • 管理 DBMS 系统中的所有文件
  • 管理页/块集合文件中的分布
  • 管理不同的页面以及元组

存储管理能够做什么

  • 文件怎么存到文件系统? DBMS使用一个文件或多个文件,很少DBMS直接使用块;
  • 页怎么存到文件? 双向链表或Page Directory方式管理;
  • Record/元组怎么存到页? 通过各种组织文件方式,将Record组织到页,常用的为堆组织文件和索引组织文件;通常使用槽来存储Record到页;变长元组、定长元组、大对象;
  • 不同数据类型怎么存到记录? 变长字符、整型、浮点类型、定点类型、大文件、Catalog;

文件

文件是 DBMS 与 OS 在存储上结合的点,文件中存储的都是页的集合

DBMS 文件分布

不同的 DBMS 使用文件方式不同,一些系统仅使用一个文件存储所有数据,如H2等;另有一些系统会使用多个文件,如MySQL等。

记录如何在页中组织

很多地方将此概念称为 文件组织 ,但这个描述很容易混淆,因为它可能被理解为记录在文件上的存储,而实际上记录不会直接与文件结合,而是依托于页来实现记录在文件上的存储。页是DBMS对文件的二次封装。

DBMS的页看到的文件是OS的文件系统中的文件,但对于DBMS的一个记录来说,所看到的实际上是多个页(看不到OS的文件) ,因为DBMS已经把文件封装为多个页了。

通常有以下几种方式描述记录在多个页上的组织:

  • 顺序组织;记录顺序的存储在多个页中。
    • Insert:直接加入到尾页的尾部记录后;
    • Delete:查找并删除,然后移动删除点后的所有记录;
    • Select:二分查找
    • 优点:检索比较快,因为是有序的;
    • 缺点:删除某些页后,需要重排序,并且移动被删除点后的所有记录。
  • Heap 组织;记录无序的存储在多个页中(大多数DBMS都是用此方式),由于是无序的,那么下一个记录可能在另一个页,因此使用Heap时,记录需要保存下一个记录的引用,即记录构成一个单向链表。无序存储一定需要记录空闲空间,涉及到删除和创建;
    • Insert:无序插入;
    • Delete:直接删除;
    • Select:从头记录遍历;
    • 优点:删除、添加都简单;
    • 缺点:查询效率低;
  • B+ Tree 有序组织;根据树形结构表示页中的记录,MySQL InnoDB引擎使用的就是此方式;在页的头中存储了记录的索引;页是索引的叶子节点,而在页中又是通过索引结构存储的所有记录;
    • Insert:有序插入到页中;
    • Delete:标识删除;
    • Select:根据页头中的索引结构获取记录;MySQL-Innodb中使用二分查找寻找记录;
    • 优点:查询等操作性能较高;
    • 缺点:结构复杂
  • Hash 组织;根据记录的某些字段计算出页中的位置;
  • 集群组织;

页如何在文件中组织(堆文件组织记录时)

记录在使用 Heap File 组织时,通常使用以下两种方式管理数据页和空闲页,将这些页管理起来会让我们能够做到对记录的操作。试想一下,如果没有任何结构管理空闲页和数据页,那么当添加记录时,应该选择哪一个页呢;当删除或查找某一个记录时,又该找哪一个页呢。

  • 链表;
  • Page Directory(页目录);

链表

  • 使用两个双向链表(涉及到移除、添加等,因此需要双向),分别表示数据页和空闲页;
  • ‘首页’ 中存储两个头节点,分别是数据页链表的头和空闲页链表的头;
  • Insert时,从首页中选择空闲头页,插入记录(这个空闲页可能是一个空白页,也可能是之前删除过记录后转变为空闲页),如果当前页满了,那么移动到数据页尾;
  • Delete时,从数据页中移除记录,并且移动到空闲页尾;
  • Select时,从首页中的数据页头部开始,依次遍历每一个页,直到找到目标记录

Page Directory

  • ‘Page Directory’ 是一些特殊的页面,这些页面中存储的是 ‘页面’ 的集合;
  • Page Directory 中包含了其下页面的状态,如剩余空间、地址等;
  • Insert/Delete 只需要操作具体的页面后,同步更新 Page Directory 内的元数据(需要保证数据一致性);
  • Select,查找特定的页时,更加容易,因为可以直接遍历目录从而得到具体的页位置;

链表和 Page Directory

链表通过空闲链表得到空闲空间页,Page Directory 通过遍历目录得到空闲空间页;

通常使用链表而不是 Page Directory,因为Page Directory需要保证一致性并且占用额外的页,增加复杂性。而在MySQL的实现中,有一个Page Directory概念,但是两者并不是一回事这里的Page Directory(服务于所有页)是管理空闲页和数据页,而MySQL的Page Directory(服务于页中所有记录)是为了实现索引

页/块

页/块 是 DBMS 对文件的二次封装,DBMS 中的索引、元数据、Record 等所有数据都是以页的方式存储在文件中

可以简单的理解为DBMS 基于操作系统文件管理之上,再次封装了一层 DBMS 自身的文件管理。

为什么需要页

如果没有页,那么 DBMS 会直接使用文件系统,那么将会非常混乱;

DBNS 页/块 vs 磁盘块

DBMS 中的页(一些系统也称为块)表示一个逻辑数据页

磁盘块表示是对磁盘的划分,通常一个DBMS的页可能包含多个物理磁盘块;

页大小

正如虚拟内存中固定的页,DBMS 中的页大小也是固定的,一般为磁盘块大小的整数倍;

为什么页大小是固定的

固定大小的页可以避免可变页删除之后的问题, 可变页删除 后由于其大小不一后续选择合适的页填充这块空的页是一个问题,而固定的页就不会产生这个问题。

为什么页大于磁盘块

存储在磁盘上的 DBMS 性能的核心在于减少磁盘的访问,而一个大页可以避免多次磁盘调用。

页的原子性问题

DBMS 的页通常都大于磁盘块大小,单个磁盘块的写操作系统会保证原子性,而涉及到多个磁盘块时操作系统无法保证原子性,因此需要DBMS自身实现页面写的原子性。

在写页面数据到多个磁盘块时,在后续磁盘块写失败后,需要能够保证原子性(要么都写成功,要么都写失败)。

页结构

  • Header;页头包含元数据、页大小、事务可见性级别、DBMS版本等信息(一些DBMS实现了自包含-即关于该页的所有信息都在当前页中可以找到);
  • Date;存储记录等;

页标识

每个页都有一个唯一标识

Data

Page Header的存储很简单,但Data的存储比较复杂,核心原因还是在于Record并不是固定大小的。

虽然Heap文件组织的无序存储 Record 解决了记录在页上的表示方式,但并未解决Record长度不一致时如何更好的管理删除和新增Record;

一般有以下几种方式来表示Record;

  • Tuple Oriented Approach(最简单方式);直接添加Record到Data尾部;
  • Slotted Pages Approach;
  • Log Structured Approach;

Tuple Oriented Approach

  • 原理
    • Header中存储 Tuples 数量,Data中存储 Tuple
  • Insert
    • 在尾部插入Tuple或从头遍历选择一个空的Tuple;
  • Delete
    • 直接删除;
      • 如果都是定长,那么不压缩;
      • 如果是变长Record,那么需要每次删除后都对data区压缩
  • Update
    • 直接修改;
      • 压根无法支持变长;
  • 优缺点
    • 优点:简单
    • 缺点:变长的tuple会有删除问题,正如固定页一样,没有变长的删除问题,而变长时删除之后会形成空间碎片,需要压缩;
  • 场景
    • 没有DBMS会使用此方式,除非是变长的tuple;

Slotted Pages Approach

  • 原理
    • 每一个 Slot Index 对应一个 Record,Slot 中存储 offset(Record 在页中的偏移位置) 和 length(Record 的长度);
    • Header 后 Data中定义一个 Slot Array 区域,一些DBMS称为 Slot Directory,用于存储所有的Slot;
    • Free Space 是为了方便理解的表述,Slot Array从前往后扩张,Records从后往前扩张,他们都属于Data部分,其中所趋近的区域就是Free Space;
    • 当Free Space不够一个Record时,重新申请新的页存放新Record
  • Insert
    • Record从后往前存储;
      • 如果Free Space不够,那么申请新页,并存储此Record;
      • 否则存储并在Slot中记录当前Record的offset和length;
  • Delete
    • 直接删除Record;
      • 一些DBMS选择移除后直接移动当前Record后续的其他Record(此时直接删除Slot Index,并且移动Slot Array);
      • 另一些DBMS选择忽略它,当下次或Free Space不够用时,再进行移动(此时设置Slot Index的值为-1,仅记录);
      • 大多数DBMS都会在Record中保存当前的Slot ID以及Page ID,可以通过此属性查看Slot的变化情况;
  • Update
    • 更新需要区分当前Record长度是否变化;
      • 未变化,则直接更新;
      • 变化则区分变大还是变小
        • 变小则更新Slot Value和压缩空间
        • 变大需要判断Free Space是否可用,不可用要 移动当前Record到新的页中,可用则移动空间中的Records并修改当前Slot Index中的length
  • 优缺点
    • 优点:灵活的存储变长、定长 Record;
    • 缺点:复杂
  • 场景
    • 大多数DBMS使用此方式,因为它能很好的解决变长Record很明显移动比压缩要好得多,offste和length可以明确知道删除Record后,后续Record移动到那个位置;
  • 为什么从后往前添加Record?
    • Slot Array 和Records 都是变长的,不能从一个方向开始;

Log Structured Approach

  • 原理
    • 页中存储的不是record,而是执行语句的日志
    • 写数据时直接追加更新语句到页尾
    • 读数据时需要获取所有语句并进行建模构造出一个record返回用户
    • 异步任务进行页的压缩,合并多条语句,如Insert A 和 Update A,合并为一条记录A;
  • Insert/Delete/Update
    • 直接将语句插入到页尾;
  • 优缺点:
    • 优点:写性能非常高,因为都是顺序写,直接追加;
    • 缺点:读必须获取所有的日志并构建为record,性能差;

定长记录、变长记录、大对象

  • 定长记录使用任意方式都可以存储,需要注意大小;
  • 变长记录在大多数DBMS都是通过slot方式在页中存储的;
  • 大对象使用两种方式存储数据(如text、varchar、blob),对于RDBMS来说大多使用以下两种方式,但实际上很大的数据应该考虑使用其他类型的DBMS存储,如NoSQL类型的MongoDB等,不同的数据由不同类型的DBMS管理,即使这些数据都所处一个系统之中,关系型小数据使用RDBMS管理、K-V数据使用NoSQL管理、大数据使用NoSQL管理;
    • 使用Overflow pages(溢出页) 存储,将具体数据存储到溢出页中,然后数据记录所在页中有一个指针存储溢出页的地址引用;
    • 使用外部扩展文件存储,将具体数据存储到DB的外部文件中,缺点在于不受DBMS完全管理;

如何在页中查找 Record

前提:页在堆中以双向链表或Page Directory方式组织、Record 在页中以堆文件方式组织、Record 以 Slot (Array)Directory 方式存储

  1. 用户执行SQL ‘SELECT xxx WHERE xx=?’;
  2. 一系列的处理后得到页面ID和Slot Number(包含缓存、解析等);
  3. 根据页面ID通过页的双向链表或Page Directory方式找到对应的页
  4. 根据Slot ID从Slot Array中找出Value,其中包含Record的偏移量以及Record的长度,通过此可以获取到Record数据;

注意:此流程在使用索引时可能不一样;

Tuple(元组)

元组在文件页中的表示只是一串字节码,而其表示的含义是一条记录,不同的DBMS的元组结构和实现方式都有所不同;

Tuple 大小

通常元组并不是固定长度的;

为什么一个元组大小不要大于一页

大于一页会使得一个记录的读取需要从磁盘中加载多次;而一些特殊的比较大的记录使用特殊的方式存储;

Tuple 结构

  • Header;
  • Data;
  • Unique Identifier;元组的标识符,大多数 DBMS 中使用page-id + slot offset(槽偏移量);如(1-0)表示当前记录在‘1’页第‘0’槽;

Record ID

标识符,使用 page-id + slot offset 标识;

不同数据类型的存储方式

整型

整型记录存储比较简单,表在创建过程中就已经对当前的Integer类型定义了固定大小的长度。

如:INTEGER, BIGINT, SMALLINT, TINYINT;

浮点类型

由于浮点类型的特性,非常容易产生精度问题(根源是小数转二进制以及符号类型存储长度限制);小数转二进制是*2取首位,然后继续取下一位,直到小数位为0;

0.1 * 2 = 0.2 =》0 剩 0.2

0.2 * 2 = 0.4 =》0 剩 0.4

0.4 * 2 = 0.8 =》0 剩 0.8

0.8 * 2 = 1.6 =》1 剩 0.6

0.6 * 2 = 1.2 =》1 剩 0.2

...... 1-00011......由于小数转二进制的方式是*2取首,因此0.1是无限的;

又因为浮点类型是有长度限制的,float通常4字节(实际尾数可用23位二进制,7-8位十进制表示),double为8字节(52位二进制表示以及15-16位十进制表示),高位为有符号标识;因此在float类型下,十进制小数转为的二进制只有23位,那么由于float的二进制长度限制,不能表示剩余的二进制数据,因此数据开始出现误差

而当float数据从二进制再次转为十进制时,根据23位不完整的二进制 转为的7位十进制,就会出现float存储后的数据 != 0.1,导致精度丢失

如:FLOAT, REAL;

参见:IEEE-754

定点类型

定点类型存储非常复杂,其需要解决精度问题,需要多个字段标识当前数据的精度、位数、字符状态等;

浮点类型的数据容易导致精度丢失,因此一般情况下现在不再使用浮点类型在DBMS中,由于定点类型约束了小数的位数,并且使用了一些算法保证了精度;

如:NUMERIC, DECIMAL;

变长字符类型

变长的字符类型通过额外的length字段表示字符长度即可,问题在于字符长度过大超过一页大小时如何处理(参考大对象的存储);

如:VARCHAR, VARBINARY, TEXT, BLOB;

时间类型

时间可以利用UNIX的规范存储为数字,在处理这些数据时将其从数字根据时间相关配置转为实际的时间即可。

如:TIME, DATE, TIMESTAMP;

元数据 与 System Catalog

元数据是描述数据的数据,DBMS中的元数据描述了系统目录、监控信息等很多数据;

System Catalog 严格的称呼应该是 Metadata System Catalog,System Catalog也是元数据,其描述的数据是数据库的系统目录,如当前库有多少、表、索引等系统整体信息;元数据存在于DBMS的很多地方,而Catalog存在于DBMS的INFORMATION_SCHEMA数据表中(这是一种通用规范),Catalog 仅仅是 DBMS 中关于系统目录的元数据描述系统目录的数据);