很多人在学数据库管理系统(DBMS)时,习惯直接从 SQL 语法入手,建表、写查询、调索引,却很少回头理解数据库底层“用什么规则推导出结果”。关系代数就是这一层最基础的语言。它是数据库理论课程里避不开的内容,也是理解 SQL、查询优化器、索引选择和事务隔离的基础。
本文围绕“关系代数查询”这个主题,用一套完整的学生-课程-选课数据作为贯穿示例,逐个拆解选择(σ)、投影(π)、并(∪)、差(−)、笛卡尔积(×)、交(∩)、连接(⋈)、除(÷)这八个基础运算。每个运算会说明定义、给出直观示例、展示计算过程,并提供对应的 SQL 写法。无论你是在准备数据库期末考试,还是想从“会写 SQL”进阶到“理解数据库是怎么工作的”,这篇内容都适用。
这是关系代数查询系列的第一部分,重点放在“基础运算”和“查询表达思想”上。第二部分会进一步讨论等值连接、自然连接与外连接的区别、查询优化中的等价变换等进阶话题。建议先跟着本文把基础运算手算一遍,再动手建表验证。
1. 为什么数据库管理系统要先学关系代数
1.1 关系代数在数据库课程中的位置
数据库管理系统(DBMS)解决的核心问题,是如何高效地存储和查询数据。大多数现代数据库产品采用关系模型,也就是把数据组织成一张张二维表。那么问题来了:当我们写出一条 SQL 时,数据库内部到底经历了什么?
大致流程是:
- SQL 语句被解析成语法树。
- 语法树被转换成关系代数表达式。
- 查询优化器对表达式进行等价变换,选择执行代价更小的方案。
- 数据库引擎按照优化后的计划扫描数据、连接表、过滤记录。
关系代数正好处在第 2 和第 3 步。它像是一种“中间语言”,把用户层面的查询意图翻译成对关系(表)的运算序列。这也是为什么数据库原理教材都会花大量篇幅介绍关系代数。
1.2 关系代数和 SQL 是什么关系
关系代数是过程化语言,它告诉系统“先做哪一步、再做哪一步”;SQL 在多数场景下是声明式语言,它告诉系统“要什么结果”,具体执行顺序由优化器决定。
举个例子,查询“计算机系学生的姓名”,关系代数的写法是:
π_Sname( σ_Dept='CS'(Student) )对应的 SQL 是:
SELECT Sname FROM Student WHERE Dept = 'CS';两者的结果完全相同。对比这两段写法,你会发现:SQL 的 WHERE 对应选择 σ,SELECT 的列对应投影 π,FROM 对应关系输入。理解关系代数,能让你更清楚地知道 SQL 的每一个子句在底层做了什么。
2. 关系模型基础概念
2.1 关系、元组、属性
关系代数操作的对象是“关系”(Relation)。在数据库里,关系就是一张表。为了精确描述运算规则,必须先统一几个概念:
- 关系:一张二维表。
- 元组:表中的一行数据。
- 属性:表中的一列数据。
- 度:关系中属性的个数。
- 基数:关系中元组的个数。
- 域:某个属性允许取值的集合。
比如一张学生表,有学号、姓名、性别、年龄、所在系 5 个属性,那么它的度是 5。如果表中有 6 行数据,基数就是 6。
2.2 关系模式与关系实例
关系模式描述表的结构,通常写作:
Student(Sno, Sname, Sex, Age, Dept)关系实例则是指某个时刻表中实际存放的数据。关系代数运算针对的是实例,也就是真实的数据行。理解这点很重要,因为同一个关系模式,在不同时刻执行同样的关系代数表达式,可能得到不同结果。
本文统一使用这样一组演示数据,后面所有运算都会围绕这三张表展开。
3. 实验环境与演示数据
3.1 环境说明
关系代数本身是抽象理论,不依赖具体数据库产品。但为了验证“关系代数表达式与 SQL 结果一致”,本文会给出可执行的 SQL 示例。这些 SQL 采用标准语法,在 MySQL 8.x、PostgreSQL、SQL Server 等主流数据库中都适用。你可以使用本地数据库,也可以使用在线的 SQL 练习平台。
版本不需要刻意保持一致,重点是理解表结构和运算逻辑。如果你的环境不支持某个运算符(例如 MySQL 8.0 之前的版本不支持 EXCEPT),可以参考文中给出的替代写法。
3.2 建表语句
下面是三张表的建表 SQL,建议直接复制到你的数据库中执行。
CREATE TABLE Student ( Sno CHAR(3) PRIMARY KEY, Sname VARCHAR(20), Sex CHAR(2), Age INT, Dept VARCHAR(20) ); CREATE TABLE Course ( Cno CHAR(2) PRIMARY KEY, Cname VARCHAR(50), Credit INT ); CREATE TABLE SC ( Sno CHAR(3), Cno CHAR(2), Score INT, PRIMARY KEY (Sno, Cno) );Course 表存储课程信息,SC 表是选课表,记录每个学生选修了哪门课以及成绩。Sno 和 Cno 合在一起作为联合主键,表示一个学生同一门课只能有一条选课记录。
3.3 插入演示数据
插入数据的 SQL 如下:
INSERT INTO Student VALUES ('001', '张三', '男', 20, 'CS'), ('002', '李四', '女', 19, 'CS'), ('003', '王五', '男', 22, 'MA'), ('004', '赵六', '女', 21, 'CS'), ('005', '钱七', '男', 20, 'IS'), ('006', '孙八', '女', 23, 'MA'); INSERT INTO Course VALUES ('C1', '数据库原理', 4), ('C2', '操作系统', 3), ('C3', '计算机网络', 3); INSERT INTO SC VALUES ('001', 'C1', 92), ('001', 'C2', 85), ('001', 'C3', 88), ('002', 'C1', 90), ('002', 'C3', 78), ('003', 'C2', 82), ('003', 'C3', 91), ('004', 'C1', 68), ('004', 'C2', 79), ('006', 'C1', 85), ('006', 'C2', 87);数据说明:
- Student 表有 6 名学生,其中 001、002、004 属于计算机系 CS。
- Course 表有 3 门课程。
- SC 表记录了选课关系,例如 001 同学选修了全部 3 门课程。
- 005 号同学没有选修任何课程,这个数据在后面差运算示例中会用到。
下面进入全文最核心的部分:八个关系代数基础运算。
4. 八个基础运算精讲
4.1 选择 σ:按条件筛选行
选择运算记为 σ,读作 sigma。它从一个关系中筛选出满足给定谓词条件的元组,相当于 SQL 中的 WHERE 子句。
定义形式:
σ_条件(关系)其中条件是由属性名、常量、比较运算符(=、≠、>、<、≥、≤)和逻辑运算符(∧ 与、∨ 或、¬ 非)组成的布尔表达式。
示例:查询计算机系(CS)的全部学生。
σ_Dept='CS'(Student)计算过程:扫描 Student 表中的每一行,检查 Dept 属性是否等于 'CS',保留满足条件的行。
结果如下:
| Sno | Sname | Sex | Age | Dept |
|---|---|---|---|---|
| 001 | 张三 | 男 | 20 | CS |
| 002 | 李四 | 女 | 19 | CS |
| 004 | 赵六 | 女 | 21 | CS |
对应的 SQL:
SELECT * FROM Student WHERE Dept = 'CS';选择运算有两个容易出错的地方。第一,它返回的关系与原关系有相同的属性结构,不会减少列。第二,条件中的属性名必须真实存在于被操作的关系中,否则在关系代数表达式的合法性检查阶段就会失败。
再来一个复合条件示例:查询计算机系并且年龄不小于 20 岁的学生。
σ_Dept='CS' ∧ Age >= 20(Student)结果:
| Sno | Sname | Sex | Age | Dept |
|---|---|---|---|---|
| 001 | 张三 | 男 | 20 | CS |
| 004 | 赵六 | 女 | 21 | CS |
对应的 SQL:
SELECT * FROM Student WHERE Dept = 'CS' AND Age >= 20;多次选择可以写成链式形式,例如 σ_Dept='CS'( σ_Age >= 20(Student) ),但通常会化简为单个复合条件。
4.2 投影 π:按列筛选字段
投影运算记为 π,读作 pi。它从关系中选出指定的列,并去掉重复行,相当于 SQL 中 SELECT 后面跟指定列的效果。
定义形式:
π_属性列表(关系)示例:查询所有学生的姓名和所在系。
π_Sname, Dept(Student)结果:
| Sname | Dept |
|---|---|
| 张三 | CS |
| 李四 | CS |
| 王五 | MA |
| 赵六 | CS |
| 钱七 | IS |
| 孙八 | MA |
这里有个关键特性:投影结果中的重复元组会被自动消除。如果查询结果里两行在所有投影属性上完全相同,只保留一行。
例如查询学生的性别:
π_Sex(Student)结果只有两行:
| Sex |
|---|
| 男 |
| 女 |
虽然 Student 表中有 6 行数据,但性别只有两种取值,投影得到的关系基数是 2。这一点和 SQL 的默认行为不同:SQL 的 SELECT 默认不去重,需要显式加 DISTINCT 才会去重。
SELECT DISTINCT Sex FROM Student;投影运算的结果列顺序、列名通常与属性列表一致。如果后续还需要使用学生的其他属性,则不能只投影一部分列,必须重新联系原始关系或使用更完整的投影列表。
投影和选择经常组合使用。查询计算机系学生的姓名:
π_Sname( σ_Dept='CS'(Student) )对应的 SQL:
SELECT Sname FROM Student WHERE Dept = 'CS';运算顺序是先选择、再投影。选择负责缩小行范围,投影负责缩小列范围,二者一横一纵,是关系代数里最常用的组合。
4.3 并 ∪:合并两个兼容关系
并运算记为 ∪。它将两个结构兼容的关系合并,保留所有元组并去掉重复项,相当于 SQL 中的 UNION。
两个关系进行并运算的前提是:
- 属性数量相同。
- 对应位置属性的域相同或兼容。
这种关系称为“并兼容”。实际使用中,最稳妥的做法是确保两个关系来自同一张表的同一结构,或者都是通过相同属性列表投影得到的临时关系。
示例:查询年龄小于等于 20 岁的学生,并上计算机系的学生。
第一个关系 R1 是年龄小于等于 20 岁的学生:
R1 = σ_Age <= 20(Student)R1 结果:
| Sno | Sname | Sex | Age | Dept |
|---|---|---|---|---|
| 001 | 张三 | 男 | 20 | CS |
| 002 | 李四 | 女 | 19 | CS |
| 005 | 钱七 | 男 | 20 | IS |
第二个关系 R2 是计算机系学生:
R2 = σ_Dept='CS'(Student)R2 结果:
| Sno | Sname | Sex | Age | Dept |
|---|---|---|---|---|
| 001 | 张三 | 男 | 20 | CS |
| 002 | 李四 | 女 | 19 | CS |
| 004 | 赵六 | 女 | 21 | CS |
并运算 R1 ∪ R2 的结果:
| Sno | Sname | Sex | Age | Dept |
|---|---|---|---|---|
| 001 | 张三 | 男 | 20 | CS |
| 002 | 李四 | 女 | 19 | CS |
| 005 | 钱七 | 男 | 20 | IS |
| 004 | 赵六 | 女 | 21 | CS |
001 和 002 在 R1、R2 中都出现,但并结果中只保留一次。注意结果中行的顺序在理论层面没有意义。
对应的 SQL:
SELECT * FROM Student WHERE Age <= 20 UNION SELECT * FROM Student WHERE Dept = 'CS';如果换成 UNION ALL,则会保留重复行,对应关系代数中不考虑去重的“多重集并”。标准关系代数默认去重,因此与 UNION 更对应。
4.4 差 −:从一个关系减去另一个关系
差运算记为 −。它从第一个关系中删除出现在第二个关系中的元组,相当于 SQL 中的 EXCEPT(MySQL 8.0 开始支持)或 Oracle 中的 MINUS。
同样要求两个关系并兼容。
示例:查询计算机系学生中,年龄小于 21 岁的学生以外的其他学生。
先求出计算机系学生:
R1 = σ_Dept='CS'(Student)再求出计算机系中年龄小于 21 岁的学生:
R2 = σ_Dept='CS' ∧ Age < 21(Student)最后计算差:
R1 − R2R1 是 001、002、004 三个计算机系学生,R2 是 001、002 两个符合年龄条件的学生。相减后结果是:
| Sno | Sname | Sex | Age | Dept |
|---|---|---|---|---|
| 004 | 赵六 | 女 | 21 | CS |
对应的标准 SQL:
SELECT * FROM Student WHERE Dept = 'CS' EXCEPT SELECT * FROM Student WHERE Dept = 'CS' AND Age < 21;在 MySQL 8.0 中可以直接运行 EXCEPT,在更早版本中可以改写为 NOT IN 或 NOT EXISTS:
SELECT * FROM Student WHERE Dept = 'CS' AND (Sno, Sname, Sex, Age, Dept) NOT IN ( SELECT Sno, Sname, Sex, Age, Dept FROM Student WHERE Dept = 'CS' AND Age < 21 );差运算经常用于表达“排除”语义。例如查询没有选过任何课程的学生,可以先求全体学生的学号,再减去选课表中的学号集合:
π_Sno(Student) − π_Sno(SC)这个表达式在本文综合案例中会再次出现。
4.5 笛卡尔积 ×:两表所有行组合
笛卡尔积记为 ×。它将两个关系的元组逐一配对,得到一个新关系。新关系的属性数量是两个关系属性数量之和,元组数量是两个关系元组数量之积。
形式:
R × S示例:Student(6 行 5 列)与 Course(3 行 3 列)做笛卡尔积,得到 18 行 8 列。
由于结果行数太多,这里只展示前几行:
| Sno | Sname | Sex | Age | Dept | Cno | Cname | Credit |
|---|---|---|---|---|---|---|---|
| 001 | 张三 | 男 | 20 | CS | C1 | 数据库原理 | 4 |
| 001 | 张三 | 男 | 20 | CS | C2 | 操作系统 | 3 |
| 001 | 张三 | 男 | 20 | CS | C3 | 计算机网络 | 3 |
| 002 | 李四 | 女 | 19 | CS | C1 | 数据库原理 | 4 |
| 002 | 李四 | 女 | 19 | CS | C2 | 操作系统 | 3 |
| ... | ... | ... | ... | ... | ... | ... | ... |
对应的 SQL:
SELECT * FROM Student CROSS JOIN Course;笛卡尔积的问题很明显:它会生成大量无意义的行。比如学生“张三”和“操作系统”组合实际上没有对应选课关系,但在笛卡尔积里会出现。因此,笛卡尔积通常不会单独用于查询,而是作为连接运算的基础。
如果直接在 SQL 中写多表连接却不加连接条件:
SELECT * FROM Student, SC;那得到的就是两个表的笛卡尔积。生产环境中这种写法非常危险,可能瞬间产生巨大的数据集。理解笛卡尔积能帮你理解为什么多表连接必须写连接条件。
4.6 交 ∩:取两个关系的共同元组
交运算记为 ∩。它返回同时出现在两个关系中的元组,相当于 SQL 中的 INTERSECT。
两个关系同样需要并兼容。
示例:查询既被 001 号学生选修,又被 003 号学生选修的课程号。
先求 001 选修的课程:
π_Cno( σ_Sno='001'(SC) )结果是 C1、C2、C3。
再求 003 选修的课程:
π_Cno( σ_Sno='003'(SC) )结果是 C2、C3。
取交集:
π_Cno( σ_Sno='001'(SC) ) ∩ π_Cno( σ_Sno='003'(SC) )结果:
| Cno |
|---|
| C2 |
| C3 |
对应的标准 SQL:
SELECT Cno FROM SC WHERE Sno = '001' INTERSECT SELECT Cno FROM SC WHERE Sno = '003';在 MySQL 8.0 之前没有 INTERSECT,可以用 IN 改写:
SELECT DISTINCT Cno FROM SC WHERE Sno = '001' AND Cno IN (SELECT Cno FROM SC WHERE Sno = '003');在关系代数体系中,交运算可以由差运算推导出来:
R ∩ S = R − (R − S)这个公式很重要,它说明并非所有运算都是基础运算。在理论教学中,通常只把选择、投影、并、差、笛卡尔积作为基本运算,交、连接、除都可以由它们组合推导。
4.7 连接 ⋈:按条件配对两张表
连接运算记为 ⋈。它本质上是笛卡尔积加选择条件的组合,用于把两张表中满足关联条件的行拼在一起。
最常见的连接形式是 θ 连接:
R ⋈_条件 S等价于:
σ_条件(R × S)示例:把 Student 和 SC 按学号相等条件连接。
Student ⋈_Student.Sno = SC.Sno SC由于属性名 Sno 在两张表中都存在,必须用“关系名.属性名”的方式限定,避免歧义。
结果包含 11 行,下表只展示其中几行:
| Sno | Sname | Sex | Age | Dept | Cno | Score |
|---|---|---|---|---|---|---|
| 001 | 张三 | 男 | 20 | CS | C1 | 92 |
| 001 | 张三 | 男 | 20 | CS | C2 | 85 |
| 001 | 张三 | 男 | 20 | CS | C3 | 88 |
| 002 | 李四 | 女 | 19 | CS | C1 | 90 |
| 004 | 赵六 | 女 | 21 | CS | C1 | 68 |
| ... | ... | ... | ... | ... | ... | ... |
连接条件中的属性名可以不同,例如按学生年龄和课程学分比较。
对应的 SQL:
SELECT * FROM Student JOIN SC ON Student.Sno = SC.Sno;如果连接条件是同名属性的等值比较,并且结果中不保留重复的关联列,就称为自然连接,记为:
Student ⋈ SC自然连接会自动找出两张表中同名属性,并按照“值相等”进行连接,同时只保留一个同名列。
自然连接的 SQL 写法是:
SELECT * FROM Student NATURAL JOIN SC;需要注意的是,自然连接要求同名属性的值确实表示同一个含义。比如 Student 中有 Age,Course 中如果也有 Age 字段,但含义是“课程适合年龄”,就会被错误连接。因此生产环境更推荐显式指定连接条件,而不是滥用手动指定的自然连接。
4.8 除 ÷:求解“全部包含”关系
除运算记为 ÷,是八大运算中最抽象、也最容易理解错的一个。它主要用于表达“至少包含所有……”这类语义。
定义可以这样理解:
R ÷ S表示从 R 中找出这样的元组:它与 S 中所有元组的组合都出现在 R 中。
在选课场景里,最经典的问题就是“查询选修了全部课程的学生”。在这里,R 是选课关系投影出的学号-课程号对,S 是全部课程号。
构造被除关系 R:
R = π_Sno, Cno(SC)R 的内容是每个学生选修的课程号。
构造除数 S:
S = π_Cno(Course)S 的内容是所有课程号。因为表中有 C1、C2、C3 三门课,所以 S 有 3 行。
除法 R ÷ S 的计算过程可以拆解为三步:
第 1 步:找出课程表中全部课程:
{C1, C2, C3}第 2 步:检查每个学生选课集合是否包含这 3 门课。
001 选了 C1、C2、C3,包含全部课程,保留。 002 选了 C1、C3,缺少 C2,排除。 003 选了 C2、C3,缺少 C1,排除。 004 选了 C1、C2,缺少 C3,排除。 006 选了 C1、C2,缺少 C3,排除。
第 3 步:得到商关系:
| Sno |
|---|
| 001 |
对应的 SQL 可以使用双重 NOT EXISTS 实现:
SELECT DISTINCT Sno FROM SC AS SC1 WHERE NOT EXISTS ( SELECT 1 FROM Course WHERE NOT EXISTS ( SELECT 1 FROM SC AS SC2 WHERE SC2.Sno = SC1.Sno AND SC2.Cno = Course.Cno ) );这段 SQL 的语义是:找这样的学生,不存在任何一门课是他没有选修过的。读起来有点绕,但逻辑和除运算完全一致。
除运算还可以表达更复杂的“全部包含”问题。比如查询选修了 001 学生所选全部课程的学生,可以把除数改为 001 的选课集合:
π_Sno, Cno(SC) ÷ π_Cno( σ_Sno='001'(SC) )001 选了 C1、C2、C3,所以结果还是只有 001 自己。
5. 综合实例:选课业务查询
前面八个运算拆开看都很简单,真正重要的是组合使用。下面用一组典型的选课查询,演示如何用关系代数表达,并与 SQL 一一对照。
5.1 查询选修了 C2 课程的学生姓名
关系代数表达式:
π_Sname( σ_Cno='C2'( Student ⋈ SC ) )计算顺序是:先自然连接 Student 和 SC,得到带学生信息的选课记录;再选择课程号为 C2 的行;最后投影学生姓名。
对应的 SQL:
SELECT DISTINCT Sname FROM Student JOIN SC ON Student.Sno = SC.Sno WHERE SC.Cno = 'C2';结果:
| Sname |
|---|
| 张三 |
| 王五 |
| 赵六 |
| 孙八 |
5.2 查询同时选修 C1 和 C2 课程的学生学号
“同时选修”在关系代数中可以通过交运算表达。
π_Sno( σ_Cno='C1'(SC) ) ∩ π_Sno( σ_Cno='C2'(SC) )对应的 SQL:
SELECT Sno FROM SC WHERE Cno = 'C1' INTERSECT SELECT Sno FROM SC WHERE Cno = 'C2';如果不能使用 INTERSECT,可以改写为:
SELECT DISTINCT Sno FROM SC WHERE Cno = 'C1' AND Sno IN (SELECT Sno FROM SC WHERE Cno = 'C2');结果:
| Sno |
|---|
| 001 |
| 004 |
| 006 |
5.3 查询没有选修任何课程的学生
没有选修任何课程,等价于“学生集合减去有选课记录的学生”。
π_Sno(Student) − π_Sno(SC)结果:
| Sno |
|---|
| 005 |
对应的 SQL:
SELECT Sno FROM Student EXCEPT SELECT Sno FROM SC;MySQL 8.0 以下可以改写为:
SELECT Sno FROM Student WHERE Sno NOT IN (SELECT Sno FROM SC);5.4 查询选修了全部课程的学生姓名
这里用到除运算。先求出选修全部课程的学生学号,再连接 Student 得到姓名。
π_Sname( Student ⋈ ( π_Sno, Cno(SC) ÷ π_Cno(Course) ) )对应的 SQL:
SELECT Sname FROM Student WHERE Sno IN ( SELECT Sno FROM SC AS SC1 WHERE NOT EXISTS ( SELECT 1 FROM Course WHERE NOT EXISTS ( SELECT 1 FROM SC AS SC2 WHERE SC2.Sno = SC1.Sno AND SC2.Cno = Course.Cno ) ) );最终结果是“张三”。因为整个演示数据中只有 001 号学生选修了全部三门课程。
5.5 查询计算机系学生选修了但成绩低于 80 分的课程名
这个问题需要连接三张表:先选计算机系学生,再连接选课表,再连接课程表,最后筛选成绩。
π_Cname( σ_Dept='CS' ∧ Score < 80( Student ⋈ SC ⋈ Course ) )对应的 SQL:
SELECT DISTINCT Cname FROM Student JOIN SC ON Student.Sno = SC.Sno JOIN Course ON Course.Cno = SC.Cno WHERE Student.Dept = 'CS' AND SC.Score < 80;结果:
| Cname |
|---|
| 数据库原理 |
在这个结果中,004 号学生赵六选修了数据库原理,成绩 68 分,低于 80,满足条件。
6. 关系代数学习中的常见误区
6.1 混淆选择与投影
选择筛选的是行,投影筛选的是列。很多初学同学写出 σ_Sname='张三'(Student) 这样看似合理但错误的表达式。Sname 是列名,用选择运算筛选时只能写“属性与常量的比较”,所以正确的写法是:
σ_Sname='张三'(Student)这样是合法的,因为条件是“姓名等于张三”。但如果你想“只看姓名这一列”,应该用投影:
π_Sname(Student)两者的区别是:选择保留整行,投影只保留指定列。
6.2 忽略并、差、交运算的兼容性
并、差、交运算要求两个关系有相同的属性数量,并且对应属性的域兼容。如果把 π_Sname(Student) 和 π_Sno(SC) 做并运算,结果就是非法的,因为一个是姓名、一个是学号,属性数量和语义不一致。
6.3 笛卡尔积不加条件
笛卡尔积会放大数据量。假设 Student 有 1000 行,SC 有 10 万行,乘积就是 1 亿行。如果不加选择条件直接用于查询,不仅结果没有意义,还会导致数据库执行计划崩溃。在 SQL 中写多表连接时,务必确认连接条件已写明。
6.4 把自然连接与等值连接混淆
自然连接基于同名属性自动连接,并且只保留一个同名列。等值连接可以基于不同属性名的等值比较,并且结果中会保留两张表原有的所有属性列。生产环境中,显式写连接条件更安全、更可控。
6.5 除运算只能记住结果形式
很多同学背下除运算的公式,却没理解它的含义。其实只要记住一句话:除运算是在问“哪些对象满足与给定集合中每个元素都存在配对关系”。建议做题时先手算两遍,再对照 SQL 理解。
7. 最佳实践与学习建议
7.1 自建老式练习法
关系代数的难点在于抽象的集合思维。最有效的练习方式是:给出一张很小的表,先手写出每次运算的中间结果,再用 SQL 验证。比如把第 4 节中的 8 个例子全部手算一遍,你会对每个运算的输入输出结构产生直观印象。
7.2 表达式书写顺序
写复杂关系代数表达式时,一般遵循“先窄后宽、先选后连”的原则:
- 先用选择缩小数据量;
- 再用投影减少列数;
- 最后做连接和集合运算。
虽然关系代数的等价变换允许调整顺序,但良好顺序能降低中间结果规模,这种习惯在理解查询优化时非常有价值。
7.3 对照 SQL 学习
每学一个关系代数运算符,就把它与 SQL 对应起来:
- σ 对应 WHERE;
- π 对应 SELECT 列列表;
- ∪ 对应 UNION;
- − 对应 EXCEPT / MINUS;
- × 对应 CROSS JOIN;
- ∩ 对应 INTERSECT;
- ⋈ 对应 JOIN;
- ÷ 对应 NOT EXISTS 嵌套查询。
这种对照关系能帮助你快速从理论切换到实际开发。
7.4 不要忽略空值与重复元组
标准关系代数建立在集合论上,默认不存在重复元组,也不涉及空值。但真实数据库表允许 NULL 和重复行。因此,在 SQL 中验证关系代数表达式时,可能会因为 NULL 比较、COUNT 统计方式不同而产生偏差。理解理论模型时以标准关系代数为准,写 SQL 时要额外考虑空值和去重。
7.5 注意安全边界与生产实践
如果要在真实业务库中执行验证查询,务必遵守以下原则:
- 只对测试库或只读库执行复杂验证;
- 涉及
UPDATE、DELETE时先开启事务或备份; - 执行大规模
CROSS JOIN前先估算数据量; - 生产环境查询优化依赖执行计划,不要认为关系代数表达式越短性能就越好。
8. 总结与下一步
说到这可能你已经发现了:关系代数查询并不难,八个基础运算各有明确职责。选择管行方向筛选,投影管列方向筛选,并、差、交管集合方向运算,笛卡尔积和连接管表与表之间的组合,除则解决“全部包含”这类复杂语义。真正拉开差距的,是能否把复杂查询拆解成这些基础运算的组合。
本文是关系代数查询的第一部分。建议你把文中的三张表建好,把 8 个运算符的例子全部手算一遍,再对照 SQL 验证结果。这个“手算 → 写 SQL → 对比结果”的过程,能帮你同时打通理论理解和动手能力。
下一步可以继续学习三块内容:一是等值连接、自然连接、外连接的区别与适用场景;二是关系代数表达式的等价变换规则,比如选择下推、投影下推;三是结合数据库执行计划,观察同一查询在不同表达式形式下的执行代价变化。这些内容会在后续文章中继续展开。
动手把示例数据建好,逐个验证一遍,比单纯背定义有用得多。