这是我在学习Databases Systems CMU 15-445/645/ Fall 2019过程记录的一些笔记,这次学习的第四课,主要是关于数据库存储的第二部分,主要是分析了 tuple 的不同存储方式。
数据表现方式 Data Representation
Tuple Storage
在内部存储的时候, tuples 只是一系列的字节数组,在使用的时候 DBMS 根据其内部存储的 schema 数据去推断 tuple 的组织方式来读取它。
数据的表现形式在不同层面有不同表现:
- C/C++ 表现方式:
INTEGER
/BIGINT
/SMALLINT
/TINYINT
IEEE-754 标准和定点小数(浮点 vs. 定点):
FLOAT/REAL
vs.NUMERIC/DECMAL
表头:
VARCHAR
/VARBINARY
/TEXT
/BLOB
- 时间表现方式(Unix epoch 之后的(微)秒数):
TIME
/DATE
/TIMESTAMP
数字的存储方式
DBMS 的用户只需要关注最后两种,前两种由开发者考虑(主要是第二个两种表现方式的区别),对于第一种而言,这种数据类型主要是用了原生的 C/C++ 的数据类型,也是符合 IEEE-754 标准的,通常来说操作会比较快(CPU 有写好的指令,不需要额外操作),但是有的时候会有 rounding 的问题,具体参考以下两段代码。
1 |
|
他的输出是:
1 | x + y = 0.300000 |
出现这样问题主要是因为硬件没办法存储固定位数的小数,为了解决 rounding 的问题,我们需要考虑用定点小数去存储数据,这个时候我们考虑用一种类似VARCHAR
的方式(不过不是存成字符串)去存储一组定长的二进制表示并且有元数据(存储位数等等)说明的方式去存储。
Postgres 内部的数字存储方式如下(也是一种定点数存储方法),对于这种方式存储,数字的操作方式会需要很多额外的考虑(为什么比浮点数计算速度慢的原因):
1 | typedef unsigned char NumbericDigit; |
大尺寸的值的存储
通常来说, DBMS 不允许单个 tuple 的尺寸超过一定值,对于 tuple 里面某些比较大的参数,有不同的处理方式:
将超过规定的值存在一个单独的页面(overflow page)里,在 tuple 内部用一个指针指向它,如下图所示,这样做的原因是当 tuple 崩溃的时候不希望会影响到这个参数,通常比较大的参数大部分时候不需要 update,常用的操作是 read,常用的 DBMS 的 overflow page 有:
- Postgres: TOAST (>2KB)
- MySQL: Overflow (>½ size of page)
SQL Server: Overflow (>size of page)
还有一种做法是将内容存在外部文件(
BLOB
类型)里面,然后用指针指向该文件的位置,如下图所示。但是 DMBS 不能更改外部文件,只能读取,而且没办法对文件保护(如果其他进程对文件进行修改的话),用这种方法的 DBMS 有:- Oracle: BFILE data type
Microsoft: FILESTREAM data type
System Catalogs
一个 DBMS 会将与数据有关的一些元数据存储到它内部的目录里:包括表格,列,索引,用户和权限还有内部统计数据等等。同时这个目录会和数据库的内容一起存在 DBMS 里。
用户可以通过查询 DBMS 的 INFORMATION_SCHEMA
目录来获得关于该数据库的内部信息:这是 ANSI 的标准规定一个只读的方式来提供关于数据库的信息。同时,不同的 DBMS 也有自己的方式(快捷键)来获得信息,如下图所示。
关系模型没有规定我们必须将 tuple 的所有参数都存放在一个单独page上,因为在某些情况下这样未必是最好的布置方式,下面以 Wikipedia 举个例子:
下图是三个 table:
我们主要考虑两种工作模式(workload):
- On-line Transaction Processing (OLTP): 每次查询从外部读取一小部分数据然后存入(更新)本地数据库中,通常是用户第一次搭建(安装)应用要做的步骤,对应的维基百科例子操作如下所示:
1 | // 更新页面 |
1 | // 登陆账户 |
1 | // 插入数据 |
- 另一种工作模式是 On-line Analytical Processing (OLAP): 一次读取大量数据计算计算处理,但是不对数据进行更新,对应的维基百科例子如下:
1 | // 计算一些统计数据 |
两种工作方式和读写关系如下图,这里不讨论HTAP:
Storage Models
在了解两种工作方式,我们可以开始考虑存储模型。DBMS 针对 OLTP 或者 OLAP 工作方式可以有不同的方式去存储 tuples。目前我们假定统一用 n-ary 存储方式(行存储)。
N-ary Storage Model (NSM)
用这种模型 DBMS 连续存储 tuple 的所有参数在一页里,tuple 之间也是连续存放。这种方式对 OLTP 比较有利,因为大部分时间只需要读取/更新/插入单条信息(tuple),如下图所示。
下面是一个简单的查询过程演示,这种情况下每次只插入一个 tuple,这种存储方式比较快捷:
对于下面的例子,NSM 的存储方式比较不好,因为查询过程不得不访问(读取)所有的 tuple,哪怕我们只需要其中两个参数,效率比较低
总结一下, NSM 的优缺点分别是:
优点:
- 快速插入,更新和删除
- 对于需要整个 tuple 的查询指令比较友好
缺点:
- 对于扫描某组参数的查询指令不友好(效率低)
DECOMPOSITION STORAGE MODEL (DSM)
针对上面的问题,还有第二种存储模型,这种存储模型按参数将所有值连续存放(也被称为列模型),对于 OLAP 工作模型(需要扫描(只读取)所有 tuple 的某个参数)比较友好,对应的例子如下所示:
在这种情况下,我们怎么识别哪个参数对应哪个 tuple 呢?有两种方法选择:
- 固定长度的偏移(fixed-length offsets):每个值保存成相同的长度;
- 嵌入 id(embedded tuple ids)每个值额外存储一个 tuple id 值用于识别,比较浪费空间。
总结一下, DSM 的优缺点分别是:
优点:
- 减少需要的I/O数(只读取需要的数据)
- 因为同一个page 保存的是相同的类型,所以可以有数据压缩的方法
缺点:
- 对于查询(插入,更新,删除)单个 tuple 效率较低。
目前绝大部分系统都有 DSM 存储方式。