数据库存储系列(4)关系模型和关系代数

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

今天我们从关系型数据库的底层模型开始说起,回归初心,看看这些支撑数据库的底层逻辑是怎样的。

数据库定义

人们经常搞混数据库和 DBMS,其实这是两个概念。数据库的定义:

A database is an organized collection of inter-related data that models some aspect of the real-world.

注意,核心在于【互相关联的数据集合】。而 DBMS 也就是我们说的【数据库管理系统】,类似 MySQL,Oracle,MongoDB 这类,说的是管理数据库的【软件】,这是两个层次。

比如 MySQL 不仅仅包含真实存储数据的文件,还有server,client,存储引擎来使得数据库的交互得以进行。本质是个 DBMS,只是通常我们不区分二者,当我们说起 DB 时,提到的就是一个 DBMS,而不只是底层存储数据的介质。

文件存储

想理解为什么我们会用 B+ 树,或者 LSM 树来存储数据,就必须从源头去理解这个事情。

设想一下此刻你没有各种业界常见的数据库实现,假设让你来设计一个数据库,会怎么做呢?

一个最基础的想法其实就是把数据存到一个【文本文件】中,比如是一个 CSV,第一行代表各个列的名称,从第二行开始就是所有的数据。类似下面这样:

id, name, age, brother
1, andy, 12, tom
2, tom, 13, andy
3, jerry, 11, alex
复制代码

这样的数据库里每一行都代表一个实体(entity)。那么当我们想去读写的时候,我们需要先来解析这个文件。比如读就代表着一次全文件扫描,一行一行看是否有数据,如果到最后还没有,就返回一个 RecordNotFound。

更新则需要遍历所有行,如果发现已经存在,就修改对应的属性(attribute),如果不存在则在最后插入一条新的。

扫描二维码关注公众号,回复: 14448980 查看本文章

这样看,其实单说读写功能上是可以满足要求的。这就是一个最基础的【数据库】。

但我们也会发现,这样做的问题在今天看来还是非常多的,以上面这个非常简单的模型为例:

数据的完整性,一致性

这里插一句,其实业界经常提到的 data integrity 是一个语义相当丰富的词,一直没有找到比较好的对应,这里引用一个比较好的定义:

Data integrity is the overall accuracy, completeness, and consistency of data. Data integrity also refers to the safety of data in regard to regulatory compliance — such as GDPR compliance — and security. It is maintained by a collection of processes, rules, and standards implemented during the design phase. When the integrity of data is secure, the information stored in a database will remain complete, accurate, and reliable no matter how long it’s stored or how often it’s accessed.

简单说,所谓 data integrity 不仅仅指数据的完整性,也包含一致性,安全性,准确性。

那么针对我们上面的 CSV 文件存储,就会存在一些问题:

  1. 从业务语义上来说,一个id 对应的 name 是确定的,但如果有人往里面写了不一致的呢?比如已经存在了 (1, andy),但又有人写入了(1, harry),这样数据一致性就被破坏了。

(从现在的眼光,我们需要加上 unique key,保证一个 id 只能对应一个 name)

  1. 如果有人把 age 这里写了个非法字符,比如 "78fafwf" 怎么办?

(我们需要数据类型的校验)

  1. 如果一个人的 brother 不止一个怎么办?

(需要考虑对于一对多的场景模型怎么设计,是否需要通过另一个表关联)

实现问题

  1. 如何实现找到一个记录?是否必须全表扫描才行?
  2. 如果我们想让另一个业务程序也用一样的数据库,怎样支持?应该怎样暴露接口去支持?
  3. 如果存在多个线程同时往这个 CSV 文件里写记录,怎么办?是否出现并发覆盖等问题?

持久化

  1. 如果我们的数据库正在往 CSV 文件里更新一条记录,这个时候宕机了,会发生什么?是否数据完整性就被破坏了?比如只有一个 id 写入,没有 name 等属性。
  2. 如果我们想将这个 CSV 文件数据库做成多副本来支持高可用,怎么实现呢?

针对上面提到的三个方面:数据完整性,实现复杂度,持久化。我们可以逐步理解到设计一个数据库,以及一套DBMS 的难度。很多现实世界的因素都需要考虑。

这对于1970年以前的计算机科学家们来说也是一样要思考,而且非常头疼的问题。下面我们来看看关系型数据库的第一步是怎样走出来的。

DBMS

一开始我们简单提到了,所谓 DBMS 就是支持与数据库进行交互的软件。那么这些交互有哪些呢?

通常来说,一个通用的 DBMS 至少需要支持:结构定义,新建,查询,更新,运维 这五种操作。

早期的 DBMS 很痛苦的地方在于:逻辑层物理存储层之间的强耦合。

  • 逻辑层感知的是数据库所包含的 实体属性

  • 物理存储层感知的是这些 实体属性 到底底层是怎么存储的。

感知逻辑层这一点没有问题,毕竟我们就是跟这些实体打交道的。但很不幸,早期的实践中,要实现查询,我们还需要显式地来写代码支持每一种实体从底层存储是怎样提取出来,找到符合业务查询语义的对象。

带来的坏处是显而易见的,如果此后你想要把底层存储切换一下,你甚至还需要改业务代码,因为业务代码是感知到存储结构的。

今天的我们,能想象如果我们现在业务代码也需要感知 MySQL 的 B+ 树,InnoDB 页的格式,行的格式,聚簇索引,二级索引等等,需要自己来实现找到自己的表以及符合要求的记录么?这简直不可思议,非常吓人。

那么有没有解法呢?当然有,从那以后,历史的脚步就开始往我们今天所熟知的方向演进。

关系模型

在 1970 年,Ted Codd 提出了【关系模型】,为了解决掉【业务代码】感知【存储结构】这个难题。

所谓【关系模型】包含三个要点:

  1. DB中的数据要存储在一种简单的数据结构 relation 中;
  2. 访问数据要通过【高级语言】来实现,而不是直接感知存储结构;
  3. 底层存储的结构不做约束,由提供数据库的厂商自行实现。

其中,高级语言这一点我们多多少少是能感知到的,因为万事不决加一层proxy,底层存储可能很复杂,也可能比较简单,但这不应该是业务代码应该关心的。我们应该有一个中间层,约定好业务代码和中间层交互的格式就好,至于底层存储结构设计成什么样都不要紧,只要中间层按照我们约定的协议来支持 CRUD 即可。

这个高级语言,就是后来成为关系模型事实标准的 Structured Query Language,即 SQL。

关系模型包含三个概念:

  • Structure:对于 relation 的定义,以及包含的内容。这决定了 relation 能够拥有什么属性,以及 relation 能够承载的数据;
  • Integrity:保证数据库的内容满足约束。比如我们定义了一个字段是年份,那我们可以至少施加约束,这个字段只能是数字。
  • Manipulation:如何 读取 或 更改 数据库中的内容。

那么这个 relation 是什么呢?我们参照一下官方定义:

A relation is an unordered set that contains the relationship of attributes that represent entities. Since the relationships are unordered, the DBMS can store them in any way it wants, allowing for optimization.

理解了么?其实 relation 是一种模型化的描述,比如我现在有一张 User 表,这里面有 name, age, weight 属性,那么 (name, age, weight) 就组成了一种关系(relation)。

所谓关系型数据库也是指,我们的DB里存储的是一种种关系。而现在,作为开发者我们感受到的这个 relation,本质就是 table。所谓【关系】,才是一个 table 的内涵:一张表,代表一组关联关系。

还有一个要点不要忽略,relation 是一个无序的集合,DBMS 可以按任何方式存储,不保证顺序。

我们通常理解的一行,或者说一个 Record,其实从模型化语言的角度,被称之为 tuple,一个 tuple 代表的 relation 中的一组属性的值。这些值通常是原子的(也就是说本身不能还是个数据集合,比如 list/set)。但实际上现在 SQL规范也扩充了,可以支持一些嵌套的数据结构。

每一个 relation 中的 attribute,可以由自己的类型,但一定都有一个特殊的值:NULL。它代表了在这个 tuple 中,这个属性是未定义(undefined)。

一个拥有 n 个属性的 relation 被称为 N-ary relation。

正如 table 有主键一样,其实最早提出的关系模型的 relation 就有自己的 primary key,用于标识一个唯一的 tuple。只是一些 DBMS 在建表时没有定义主键的场景下,会内部自动给定义一个主键列。

外键同样是在初版的关系模型中就已经提出,它代表了一个relation中的属性必须和另一个relation中的属性相对应。

DML

所谓 DML,指的是 Data Manipulation Language,我们平常用的 select, update, insert, delete 都在这个范畴。DML 有两种实现思路:

  • 指令式:查询语句本身指明了 DBMS 应该怎样去找到想要的数据,给出了步骤;
  • 声明式:查询语句只是告诉 DBMS 要什么样的数据,把具体的步骤留给 DBMS 自己去搞定。

当然,我们依赖的 SQL 就是个声明式的语言。

到今天,通常依赖关系型数据库的业务发出的查询语句只是【我需要满足 XXX 条件的记录,然后筛出来 XX,XX 两个列给我】。

而不是【你来做个全表扫描,发现有匹配 XXX 条件的,就把它放到一个数组里,扫描完毕后把数组遍历一遍,过滤出来两个列返回】。

这样的隔离,保证了业务层不去感知底层,一方面业务的心智负担小,另一方面也给 DBMS 留下了充足的优化空间(语句是否可以简化,索引能否用上,缓存能否用上)。

关系代数

这里其实偏数学,大家可以从中理解 SQL 这一套体系设计之前的时代里,大家是怎么看待问题的。我们简单列举几个:

关系代数指的是支持 relation 中的 tuple 的查询/更新进行的一组基础操作。每个操作可以接受一个或多个 relation,并输出一个 relation 作为结果。下来我们来感受一下它跟今日的 SQL 语句之间的相似性和区别。

Select

接受一个 relation,输出这个 relation 中满足给定条件的的一组 tuple,你可以将其理解为是一个过滤器。当然对于这个给定条件,也支持 and, or,not 这些逻辑操作。

其实对应到 SQL,这个 Select 操作就是 Where 语句。

Projection

接受一个 relation,输出这个relation 中包含指定属性的 tuple。当然,你可以调整输出的 relation 中属性的顺序,或做一些操作。

对应到 SQL,所谓 Projection 就是 Select 语句。

Union

接受两个 relation,输出一个包含了两个 relation 中所有 tuple 的 relation。注意,使用前提是输入的两个relation必须包含完完全全一样的属性。

Intersection

和 Union 对应,也是接受两个 relation,且他们的属性必须完全一致。区别在于输出的是两个 relation 中存在交集的 tuple。

Difference

接受两个 relation,输出所有在第一个 relation 但不在第二个relation 中的 tuple。同样,属性要完全一致。

Product

接受两个 relation,输出一个包含了两个 relation 中所有属性组合的 tuple 集合,也就是笛卡尔积。

Join

接受两个 relation,输出所有两个 relation 中相同的属性都相等的 tuple 集合。

对应 SQL,这其实是 Join 语句的升级版,即所有相同定义的列的值都需要相同,而不只是指定一列相同即可。

总结

基于关系模型,前人封装出了关系代数,它是一个指令性语言。我们可以看到,针对一个关系代数操作,只要给定集合,结果就是确定的。它其实是个操作步骤。而不是声明式的。

如果一次查询,可以被组装为两个关系代数应用后的结果,那么调整两个操作的顺序,很有可能带来巨大的性能差异,虽然结果是一样的。

所以,更好的解决办法是,作为开发者,我们提出声明式的诉求,让 DBMS 自行判断应该怎么拆解问题,怎样应用优化策略来以最低的成本返回结果。

SQL 其实就是用来干这件事的,也是目前事实上的关系数据库标准。接下来我们会花更多章节来讨论 SQL 和关系型数据库底层的原理。感谢阅读!

参考资料

猜你喜欢

转载自juejin.im/post/7128791782120226830