UP | HOME

SQL 与关系代数

目录

1 前言

SQL 是基于关系代数的查询语言,假如学习过 SQL 和关系代数,你就会发现,在 SQL 的查询语句中你会发现很多关系代数的影子。

然而,虽然知道 SQL 和关系代数密切相关,但是我也不知道学了关系代数对学习 SQL 有什么好处,因此,如果你对关系代数没兴趣的话,现在就可以关掉这篇博客了。

2 关系与表

我们可以把数据库中的表和关系代数中的关系看做是一个东西,这里给出接下来会用到的两个关系(表):

  • User:

    +------+---------+--------+
    | id   | account | passwd |
    +------+---------+--------+
    |    1 | 123     | ****** |
    |    2 | 456     | ****** |
    +------+---------+--------+
    
  • Profile

    +------+------+------+
    | id   | name | age  |
    +------+------+------+
    |    1 | tony |   16 |
    |    3 | john |    2 |
    +------+------+------+
    

3 关系代数的基本运算

关系代数的基本运算包括:选择、投影、并、集合差、笛卡尔积和更名。

3.1 投影

这里我们可以先来看一看 投影 运算,它的作用和 SQL 中的 SELECT 基本相同。

比如说我们要选择 User 中的 account, 用 SQL 编写的话就是:

SELECT account FROM user;

如果用关系代数来写的话,就可以写成 \(\prod_{account}(user)\).

选择多列就可以这样: \(\prod_{id,account}(user)\).

3.2 选择

由于一些历史原因,关系代数中的选择和 SQL 中的 SELECT 不是一个意思,而是更接近 WHERE, 我们可以通过选择运算选择关系中符合指定条件的部分。

比如说 \(\sigma_{id=1}(user)\) 可以选择关系 Userid 等于 1 的用户,其等价的 SQL 语句如下:

SELECT * FROM user WHERE id = 1;

选择运算中可以使用的谓词包括: \(=, \neq, <, \leqslant, >, \geqslant\). 同时还可以使用连词 \(and(\land), or(\lor), not(\lnot)\) 将多个谓词合并为一个较大的连词。

比如说 \(\sigma_{id \geqslant 1 \land id < 3}\) 选择 id 范围在 [1, 3) 之间的用户,等价于:

SELECT * FROM user WHERE id >= 1 AND id < 3; 

同时,由于关系运算的结果依然是一个关系,因此,我们可以将关系运算组合起来,比如:选择 id 为一的用户的 account 可以表示为 \(\prod_{account}(\sigma_{id=1}(user))\)

3.3 并运算

并运算可以将两个集合并起来,对应到 SQL 中就是 UNION 操作,比如说获取 User 和 Profile 中的所有 ID:

SELECT id FROM user UNION SELECT id FROM profile;

用关系代数来表示的话就是: \(\prod_{id}(user) \cup \prod_{id}(profile)\).

关系代数的并运算和 SQL 中的 UNION 一样,要求需要并起来的关系的 是相同的,同时,比 SQL 更严格的是,关系代数的并运算还要求列的位置相同。

3.4 集合差运算

集合差运算可以从一个集合中排除另一个集合中的内容,对于到 SQL 中就是 EXCEPT 操作,比如获取 User 不在 Profile 中的所有 ID1

SELECT id FROM user EXCEPT SELECT id FROM profile;

用关系代数来表示的话就是: \(\prod_{id}(user) - \prod_{id}(profile)\).

集合差运算对不同关系的要求和并运算是相同的。

3.5 笛卡尔积

笛卡尔积是一个很重要的运算,通过笛卡尔积我们可以将 任意 两个关系的信息结合在一起,笛卡尔积的运算结果会将两个关系的所有列作为新的关系的列,将两个关系的所有行的组合作为新的关系的行。

对应到 SQL 中便是 CROSS JOIN, 比如说如下 SQL 语句便可以表示为 \(user \times profile\):

SELECT * FROM user CROSS JOIN profile;

运算结果如下:

+------+---------+--------+------+------+------+
| id   | account | passwd | id   | name | age  |
+------+---------+--------+------+------+------+
|    1 | 123     | ****** |    1 | tony |   16 |
|    2 | 456     | ****** |    1 | tony |   16 |
|    1 | 123     | ****** |    3 | john |    2 |
|    2 | 456     | ****** |    3 | john |    2 |
+------+---------+--------+------+------+------+

3.6 更名运算

关系代数中的更名运算对应到 SQL 中便等价于 AS 操作,可以对关系进行更名也可以对列进行更名操作:

  • 更名关系 - \(\rho_{users}(user)\)
  • 更名列 - \(\rho_{users(uid,account,password)}(user)\)

在进行连接操作的时候常常会用到更名操作,而 SQL 中的更名操作用起来比关系代数中的方便一些,形象一些。

4 关系代数的附加运算

关系代数的附加运算是可以通过基本运算推导得出的,包括集合交运算和各类连接运算。

4.1 集合交运算

集合交运算计算两个关系中都存在的部分,可以通过基本运算表示: \(r \cap s = r - (r - s)\).

集合交运算对于的 SQL 语句是 INTERSECT, 比如:

SELECT id FROM user INTERSECT SELECT id FROM profile;

表示为关系代数便是 \(\prod_{id}(user) \cap \prod_{id}(profile)\).

4.2 连接运算

个人认为连接运算是所有运算中最难的一种,它存在很多分类,比如:自然连接、内连接、外连接等。

同时,不同的连接运算之间还存在不浅的关系,因此,需要好好理解才行。

4.2.1 自然连接

首先是自然连接,自然连接将两个关系的 属性集并集 作为新的关系的属性,同时会对两个关系中的相同属性进行比较筛选。

假如两个关系不存在相同的属性,那么自然连接的结果便和 笛卡尔积 相同:

+------+---------+--------+------+------+
| id   | account | passwd | name | age  |
+------+---------+--------+------+------+
|    1 | 123     | ****** | tony |   16 |
+------+---------+--------+------+------+

如上便是 自然连接 的运算结果,它将关系 User 和 Profile 的属性的并集作为新关系的属性,同时筛选具有相同 ID 值的行。

连接运算的关系代数形式都很复杂,这里就简单列出对应的 SQL 语句好了2

SELECT * FROM user NATURAL JOIN profile;

4.2.2 内连接

可以把内连接3 看做添加了选择语句的笛卡尔积,也就是说,计算内连接时需要先行计算出笛卡尔积,然后在根据选择条件进行选择。

比如这样的内连接操作:

SELECT * FROM user INNER JOIN profile ON user.id >= profile.id;

其结果为:

+------+---------+--------+------+------+------+
| id   | account | passwd | id   | name | age  |
+------+---------+--------+------+------+------+
|    1 | 123     | ****** |    1 | tony |   16 |
|    2 | 456     | ****** |    1 | tony |   16 |
+------+---------+--------+------+------+------+

这里可以对照笛卡尔积的计算结果进行理解:

+------+---------+--------+------+------+------+
| id   | account | passwd | id   | name | age  |
+------+---------+--------+------+------+------+
|    1 | 123     | ****** |    1 | tony |   16 |
|    2 | 456     | ****** |    1 | tony |   16 |
|    1 | 123     | ****** |    3 | john |    2 |
|    2 | 456     | ****** |    3 | john |    2 |
+------+---------+--------+------+------+------+

4.2.3 外连接

我们可以把外连接看做是 内连接 的扩展4,首先计算出两个关系内连接的结果,然后根据外连接的类型补充数据到内连接的结果上。

比如说左外连接,首先可以计算出 User 和 Profile 的内连接,然后用空值来填充在左侧关系中存在而右侧关系中不存在的项就可以了。

SELECT * FROM user LEFT JOIN profile on user.id = profile.id;

这条 SQL 语句的执行结果为:

+------+---------+--------+------+------+------+
| id   | account | passwd | id   | name | age  |
+------+---------+--------+------+------+------+
|    1 | 123     | ****** |    1 | tony |   16 |
|    2 | 456     | ****** | NULL | NULL | NULL |
+------+---------+--------+------+------+------+

如果将其替换为内连接的话便是:

+------+---------+--------+------+------+------+
| id   | account | passwd | id   | name | age  |
+------+---------+--------+------+------+------+
|    1 | 123     | ****** |    1 | tony |   16 |
+------+---------+--------+------+------+------+

可以看到,ID 为 2 的项只存在于 User 中而不存在与 Profile 中,因此,左外连接时使用了空值来填充 Profile 对应的部分,保证 User 的每项都存在。

依次类推,右外连接、全外连接也就好理解了:

  • 右外连接的执行结果:

    +------+---------+--------+------+------+------+
    | id   | account | passwd | id   | name | age  |
    +------+---------+--------+------+------+------+
    |    1 | 123     | ****** |    1 | tony |   16 |
    | NULL | NULL    | NULL   |    3 | john |    2 |
    +------+---------+--------+------+------+------+
    
  • 全外连接的执行结果:

    +------+---------+--------+------+------+------+
    | id   | account | passwd | id   | name | age  |
    +------+---------+--------+------+------+------+
    |    1 | 123     | ****** |    1 | tony |   16 |
    |    2 | 456     | ****** | NULL | NULL | NULL |
    | NULL | NULL    | NULL   |    3 | john |    2 |
    +------+---------+--------+------+------+------+
    

其实,这三个外连接是可以互相转换的,将两个关系的位置换一下就可以将左外连接转换为右外连接,而将左右外连接的结果并起来就可以得到全外连接了。

5 结语

虽然说关系代数和 SQL 有不浅的关系,但是学了关系代数,对编写 SQL 也没有多大的帮助 @_@

而且,不同的数据库实现 SQL 的语法还存在细微的差别……

也许,可以借助关系代数表达式来生成 SQL 语句!

其实关系代数还有一些扩展运算,对应到 SQL 中便是聚集、分组之类的,博客中没有说到,有兴趣的话可以去了解一下。

或者什么时候有时间了补上 @_@

脚注:

1

不同数据库对 EXCEPT 子句的支持存在区别,这里的 SQL 语句不一定能运行通过

2

如果有兴趣可以看看书《数据库系统概念》中的描述

3

其实在关系代数中内连接应该叫做 theta 连接, 这里主要是为了和 SQL 相对应

4

其实按照书《数据库系统概念》中的描述的话应该是 自然连接, 但是实际的操作结果更符合 内连接, 虽然说,内连接也可以看做是自然连接

版权声明:本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可