关系代数查询是数据库管理系统这门课里最容易被低估的内容。很多人以为它就是一套考前突击的符号规则,学完 σ、π、⋈ 就丢到一边,实际工作里写 SQL 根本用不上。我工作这些年发现正好相反:能不能看懂关系代数表达式,直接决定了你能不能读懂数据库的执行计划,能不能理解一条慢查询为什么慢,以及优化器为什么敢把一个 JOIN 改写成另一种连接顺序。关系代数就是 SQL 背后那套底层的查询语言,理解了它,你看到的就不再只是表和行,而是关系、元组和操作符。
这篇文章对应的是关系代数查询的第一部分,主要讲基础操作:选择、投影、改名、并、差、笛卡尔积,以及如何用这些基础操作组合出交和连接。适合正在学数据库管理系统原理的同学,也适合想补执行计划基础知识的后端开发。我的建议是,学的时候不要只看符号定义,要拿一张真实的小表,一个操作一个操作推演输出结果,一直推到能用自己的话解释"为什么这个表达式能查出这条数据"为止。
先说明一点:关系代数操作的输入和输出都是关系。这个性质很重要,它意味着任何操作的输出还能继续作为下一步操作的输入,所以表达式可以一层套一层,复杂查询也因此能被拆成若干基础操作的组合。
1. 关系代数不是一门符号课,它是查询优化器的底层语言
1.1 关系代数到底在做什么
关系代数(Relational Algebra)是一种过程式查询语言。这里"过程式"是关键:你写 SQL 时只声明要什么数据,数据库自己决定怎么拿;你写关系代数时,必须自己把"先做哪一步、再做哪一步"全部表达出来。比如"查计算机系年龄大于 20 的学生",SQL 写出来是一个 WHERE 条件,关系代数则是先做选择 σ,再做投影 π,顺序直接写在表达式里。
这套操作定义在关系模型之上。关系(Relation)对应日常说的表,元组(Tuple)对应行,属性(Attribute)对应列。每次操作接收一个或两个关系作为输入,产出一个新的关系。这个"闭包"特性让关系代数可以嵌套:投影的结果可以继续做选择,选择的结果可以再做连接,最终得到一个完整的查询表达式。
数据库管理系统在执行一条 SQL 时,会把它解析成关系代数表达式树或者等价形式,再做逻辑优化和物理优化。这意味着,你在学习"慢查询为什么慢"、看 EXPLAIN 输出、理解索引为什么能用上或不能用上的时候,其实都在和关系代数的各种等价改写打交道。没有这层基础,执行计划对你来说就只是一个黑盒。
1.2 开始之前先建立三个认知
第一个认知是术语转换。以后看到"关系",大脑里要知道这是"表";看到"元组",要知道这是"一行记录";看到"属性",要知道这是"一列字段"。这个转换熟练之后,再读教材里的表达式就不会被术语卡住。
第二个认知是关系代数的集合语义。理论上关系是集合,集合里不会出现重复元素,所以投影操作会隐含去重。注意这和 SQL 的表不一样,SQL 表默认是多重集合,允许重复行。很多初学者在这里迷糊:为什么关系代数投影结果没有重复行,而 SQL 里 SELECT 某一列还会出现重复?因为 SQL 为了性能默认保留重复,必须加 DISTINCT 才接近投影的效果。这个差异很关键,考试和实际读执行计划时都会碰到。
第三个认知是操作的组合性。任何一个操作的结果仍然是关系,所以表达式可以树状嵌套。学习时不要背十几个独立的公式,而是理解清楚每个操作改变什么、保留什么,然后像搭积木一样组合。
2. 先跑通三个最基础的操作:选择、投影、改名
2.1 选择(σ):按条件过滤行
选择操作符写作 σ<条件>(关系),作用是从关系里挑出满足条件的元组。它改变的是行数,不改变列数。比如有一张学生表:
students(sid, name, age, major)
关系代数写法:
σ age > 20 (students)
意思是取出年龄大于 20 的学生行。结果仍然有四列,只是行数变少了。条件也可以组合,比如:
σ age > 20 ∧ major = '计算机' (students)
逻辑与用 ∧ 表示。我一般会让学生先练选择,因为它最容易验证:拿一张 10 行左右的小表,手算条件,再对照结果。这里最容易踩的坑是条件里写了表里不存在的属性,或者把字符串比较和数值比较混在一起。还要注意,条件是作用在原始关系的属性上的。如果前面已经做过投影,把某些列去掉了,这里就不能再引用那些列。
2.2 投影(π):按列取出需要的属性
投影操作符写作 π<属性列表>(关系),作用是选出若干列,去掉其余列。它改变的是列数,行数可能也会减少,因为集合语义下去重会让重复行合并。
π name, major (students)
结果只有 name 和 major 两列。如果两个学生同名且同专业,理论结果里只会出现一行。这一点和 SQL 行为不同,SQL 要写出 SELECT DISTINCT name, major 才能得到同样效果。
选择是横向过滤,投影是纵向裁剪,这两者配合是几乎所有查询的第一步。动手练习时,反复确认两个性质:σ 不能增加列,π 不能过滤行。把这两个性质记牢,后面很多判断都会快很多。
建议写表达式之前先问自己一句:这个操作是要减少行,还是要减少列?目标和操作符对不上,表达式一定写错了。
2.3 改名(ρ):解决重名和自连接问题
改名操作符 ρ<新名称>(关系),或者 ρ<新属性1, 新属性2, ...>(关系),可以给关系换名字,也可以给属性换名字。初学者往往觉得改名不重要,实际上它是后面学连接、学自连接时绕不开的操作。
举一个典型例子:员工表 employee(emp_id, name, manager_id),要查"每个员工的上级叫什么名字"。你需要把 employee 和它自己连接,让员工的 manager_id 去匹配另一个员工副本的 emp_id。如果两个副本都还叫 employee、列都还叫 emp_id,连接条件里就不知道哪个 emp_id 是哪个角色。这时就要用改名:
ρ e1 (employee), ρ e2 (employee)
把同一张表变成逻辑上的两张表,再做连接,条件才能写清楚。改名操作本身不产生新数据,它只是给后续表达式提供名字空间,但这个动作在复杂查询里非常常见。我见过不少人在做自连接题目时卡住,最后发现不是不理解连接,而是忘了给其中一个副本改名。
3. 集合操作:并、差、笛卡尔积
3.1 并(∪)和差(−)要求表结构兼容
并操作是把两个关系的元组合并到一个结果里,差操作是从第一个关系里去掉第二个关系里也存在的元组。它们有一个硬性前置条件:两个关系必须兼容,也就是属性数量相同,且对应位置的属性来自同一个域。简单说,列数一样,每一列的数据类型和语义要对得上。
比如有两张表:
cs_students(sid, name) math_students(sid, name)
"计算机系学生加上数学系学生":
cs_students ∪ math_students
"只在计算机系名单里、但不在数学系名单里的学生":
cs_students − math_students
兼容性检查经常被忽略。我在看课程作业时发现最常见的错误,就是拿两个列数不同、或者第二列一个是姓名一个是年龄的表去做并和差,结果完全没意义。还有一点,差操作的输出一定是从第一个关系里选出来的,所以它不会产生第一个关系里没有的元组。
3.2 笛卡尔积(×):每一行都要和另一张表的每一行组合
两个关系做笛卡尔积,结果是所有元组的组合。如果第一个关系有 m 行 n 列,第二个有 k 行 l 列,结果就是 m×k 行,n+l 列。行数和列数都会迅速膨胀,所以现实生产环境里几乎不会直接使用纯笛卡尔积,但它是一切连接操作的地基。
students × courses
结果里每一行都是一个学生和一个课程的配对。单独看,这个结果大部分是无意义的组合,因为没有任何条件约束学生和课程的匹配关系。连接操作(JOIN)本质上就是"先做笛卡尔积,再用连接条件做选择"的组合。理解了这一步,你就知道为什么没有连接条件的多表查询会产生大量无关数据,也能理解为什么数据库优化器那么重视连接顺序。
纯笛卡尔积在实际查询里很危险,结果行数会按两个表的行数相乘增长。要不要写连接条件,不是风格问题,是正确性和性能问题。
3.3 做复合表达式时注意操作顺序
集合操作可以组合。比如:
π name (σ major = '计算机' (students)) − π name (σ age < 20 (students))
这个表达式的意思是先筛出计算机系学生的姓名,再筛出年龄小于 20 的学生的姓名,最后取差集,得到"计算机系里年龄不小于 20 的学生姓名"。
写复合表达式时,运算顺序决定结果。括号要写清楚,不要靠记忆优先级,因为不同教材在交集、差集这类操作上的约定不完全一致。我自己的习惯是从最内层开始读:先找最里面的括号,确定它输出什么关系,再往外一层推。这个方法对考试和阅读论文里的表达式都很管用。
4. 派生操作:交和连接其实不用单独背
4.1 交集(∩)可以用差集推导
关系代数里有些操作是基础操作,有些是派生操作。交集就是一个典型派生操作,定义是:
R ∩ S = R − (R − S)
也就是说,两个关系的交集等于"R 减去 R 里那些也属于 S 的部分"。理解这个推导,比死记交集符号有意义得多。只要保证 R 和 S 兼容,就能用并、差两个操作组合出交。
这也回答了一个常见疑问:为什么教材里有时说基础操作只有六个,有时又说有八个?因为选择、投影、并、差、笛卡尔积、改名是基础操作,交、连接都可以由它们推导出来。如果考试里问"哪些是基本操作",你要能说清这个区分。
4.2 θ 连接和自然连接是怎么组合出来的
连接操作可以分成几类。θ 连接是这样定义的:
R ⋈θ S = σ θ (R × S)
先做笛卡尔积,再用连接条件 θ 做选择。如果 θ 是"相等"条件,就是等值连接;等值连接里如果结果去掉了重复的公共属性列,就是自然连接。自然连接还有一个要求:两张表里同名的属性要取值相等,然后合并成一个公共列。
写成关系代数就是:
π 所需属性 (σ R公共属性 = S公共属性 (R × S))
所以自然连接是:笛卡尔积 → 等值选择 → 去掉重复列 → 按需投影,一串组合操作。我建议你手动推一遍,拿两个只有三四行的小关系,按这四步逐步展开,比看十遍定义都有效。
4.3 为什么连接是实际查询里最重要的操作
真实业务查询很少只查一张表,多张表之间的关联基本都靠连接。连接最消耗资源,也最容易出问题:没有连接条件的笛卡尔积会让结果指数膨胀,连接列上没索引会让数据库慢到不可接受,连接顺序选错会让中间结果变得巨大。这些问题的根源都在关系代数的组合方式里。
学习第一部分时,只要能把连接拆成"笛卡尔积 + 选择 + 投影 + 改名"这四个基础操作,就已经掌握了核心。后面的自然连接、外连接、半连接都是在这些基础上的扩展,到时候再学新符号会轻松很多。
5. 表达式写完,怎么验证写得对不对
5.1 第一步:检查表结构兼容性
验证一个关系代数表达式,不要直接对着计算机跑,先从结构下手。先看每个操作的输入关系列数、列名和值域,再看输出是什么结构。尤其是并、差、交,三个操作的输入必须兼容;选择的条件下引用的属性必须存在于输入关系里;投影的列名也必须存在于输入关系里。
举个例子,σ major = '计算机' (students) 能不能写?能,前提是 students 确实有 major 列。如果你前面写了 π name (students),后面又写 σ major = '计算机',就直接无效,因为投影结果里已经没有 major 列了。这类错误在纸面推导里特别常见。
5.2 第二步:拿小数据逐行推演
结构检查通过后,拿一张只有四五行的小表,手写一遍操作过程。比如选择,把满足条件的行标出来;投影,把不需要的列划掉;笛卡尔积,按行两两组合。推演完对比预期结果,再决定下一步。
我自己的习惯是先给自己提三个问题:结果的行数大概是多少,结果的列数大概是多少,有没有哪一行数据明显不符合条件。三个问题都能答出来,才说明这个表达式是真的理解了。如果行数对不上,不要急着看下面的内容,先把操作定义再读一遍。
5.3 第三步:对照常见错误清单检查
- 选择条件里出现不存在的属性。这是最常见的结构错误。
- 投影提前删掉了后面还要用的列。推演复合表达式时尤其容易犯。
- 做并、差、交时,两个关系列数不一致或列语义不匹配。哪怕都是两列,一个装姓名一个装年龄,也不兼容。
- 笛卡尔积没有配合连接条件,导致结果行数远大于预期。
- 自连接不加改名,导致连接条件里两个 emp_id 分不清。
- 混淆集合语义和 SQL 的多重集合语义。关系代数默认去重,SQL 默认不去重,不要拿 SQL 的直觉直接套关系代数。
- 括号顺序不对,导致整体语义改变。表达式一长,每一步的输出结构都要单独确认。
这个清单不是考试技巧,是实际读执行计划、排查查询逻辑问题时同样要用的思维顺序。
6. 把关系代数和 SQL 对应起来,才算真正落地
6.1 一张表看懂对应关系
关系代数写起来像数学,但它的每个操作都能映射到 SQL 的某个子句或关键字:
| 关系代数操作 | 作用 | SQL 对应 |
|---|---|---|
| σ | 选择行 | WHERE |
| π | 投影列 | SELECT DISTINCT |
| ∪ | 并 | UNION |
| − | 差 | EXCEPT / NOT IN |
| × | 笛卡尔积 | CROSS JOIN |
| ⋈ | 连接 | JOIN ... ON |
| ρ | 改名 | AS / 表别名 |
这张表建议自己动手默写一遍,不要只看。理解映射之后,再看 EXPLAIN 输出,你会发现数据库执行计划里出现的就是扫描、过滤、哈希连接、嵌套循环连接这些物理实现,而它们背后对应的正是关系代数里的选择、投影和连接。
6.2 为什么优化器能做等价改写
关系代数有价值,还有一个重要原因:它允许等价变换。比如选择下推:在某些条件下,σ condition (R ⋈ S) 等价于 (σ on R) ⋈ S,也就是把过滤条件尽量提前,先缩小关系再连接,让中间结果变小。这是数据库查询优化最基础的手段之一。读数据库系统原理时会经常看到"启发式优化""逻辑优化"这些词,本质上都是在关系代数表达树上做等价改写。
这一点平时写 SQL 也有实际意义。当你发现一个 JOIN 查询很慢,而某些过滤条件其实可以先作用于单独一张表时,你就是在手动做"选择下推"。理解了关系代数,就不会觉得优化器的行为是玄学。
6.3 第一部分学完后,接下来学什么
关系代数查询通常会被拆成几个部分。第二部分往往会讲更复杂的连接类型:外连接、半连接、除操作,以及聚合和分组对应的扩展关系代数。第一部分的基础操作是这些内容的地基,建议先把选择、投影、改名、并、差、笛卡尔积练熟,再用它们推导交集和连接。
我的建议学习路径是:用小表手动推演每个操作 → 把表达式翻译成 SQL 对照结果 → 随意组合两个以上操作写复合表达式 → 再去看数据库执行计划验证自己的直觉。每一步都在前面检查过,后面再学新操作就不会觉得符号满天飞。
最后留一个我个人很受用的经验:关系代数适合用来把问题"想清楚",SQL 适合用来把问题"跑出来"。学第一部分的阶段,不要急着背公式,也不要急着刷题,真正值得投入时间的是拿一张真实小表,把一个复合表达式一层一层剥开,直到每层输出的行列你都心里有数。踩过几次"明明 SQL 能查出来,手写关系代数却写不出"的坑之后你会发现,问题很多不是知识不够,而是没有把选择、投影、连接这些基础操作真正当成一种思维方式。