
如果你已经能熟练地写 SQL 查询面对“为什么要学关系代数”这个问题可能会觉得它只是考试专用内容。这种想法情有可原业务开发中一条带 JOIN、GROUP BY 的 SQL 就能解决大部分数据查询需求似乎并不需要拿着希腊字母做推导。可等你真正开始排查慢查询、打开执行计划、被查询优化器的行为弄得一头雾水时就会意识到一个关键事实SQL 只是你提交给数据库的“请求文本”数据库真正在执行阶段优化和处理的是一棵基于关系代数的查询计划树。换句话说关系代数就是 SQL 和查询执行计划之间的编译中间层。你写下的 SELECT 语句会先被解析成语法树再被查询优化器转换成逻辑查询计划而逻辑查询计划使用的基础表达式正是关系代数。数据库管理系统课程几乎都会安排“关系代数查询”这个模块目的就是让你从“会写 SQL”上升到“理解数据库为什么这么执行”。Neso Academy 的数据库管理系统公开课也把这一主题拆成若干小讲本文对应其中第一部分从关系模型说起把选择、投影、并、差、笛卡尔积这五种基本运算讲透。读完这篇文章你可以做到三件事第一看懂教科书里 σ、π、∪、−、× 这些记号第二把关系代数表达式翻译成对应的 SQL第三在写复杂查询时先用关系代数的集合思维梳理逻辑减少“改了很多遍还是不对”的试错时间。本文不要求你提前掌握太多数据库理论只要你写过 SELECT 语句就能跟得上。1. 为什么 SQL 写得熟练还是需要关系代数很多人在学习数据库时会把 SQL 语法当作数据库理论的全部。这种认知在一开始没有问题增删改查、子查询、连接、聚合确实能覆盖大部分日常工作。但当查询变复杂时SQL 是一种“描述结果”的语言而不是“描述过程”的语言。你告诉数据库“我要什么”却很难用 SQL 本身说明“你怎么找最快”。而关系代数恰恰是把“找的过程”显式化的工具。举个例子。你写下这样一条查询SELECT sname FROM student WHERE college 计算机 AND age 20;数据库拿到这条语句后并不会立刻决定“先读整张表再逐行判断”。查询优化器会把它转换成多种可能的执行策略比如先对 student 表做选择筛选出计算机学院的学生再过滤年龄或者反过来先过滤年龄再过滤学院。优化器会估算每种策略的代价选它认为最小的一种。这种策略表达式如果用文字描述会很长用关系代数写出来却很紧凑π_sname( σ_college计算机 AND age20 (student) )你看一条 SQL 背后真正被执行的过程可以表达成一个关系代数表达式。理解了这一点你在看执行计划时就不会只觉得“它是一堆看不懂的树结构”而是能辨认出其中的选择、投影、连接操作分别在哪一层发生。还有一个非常现实的收益关系代数能帮助你在写 SQL 之前先做逻辑推导。比如“查询选了 C001 课程但没有选 C002 课程的学生”如果直接上手写 SQL容易在 JOIN 和子查询之间反复横跳但如果先用关系代数写出“选了 C001 的学生集合减去选了 C002 的学生集合”SQL 怎么写就一目了然了。这也是为什么很多数据库教科书的习题要求先写关系代数表达式再写 SQL。2. 关系模型三要素关系、元组、属性在讨论关系代数之前必须先统一几个基本概念。关系模型用数学化的方式描述数据表它有三个核心要素。第一个是关系Relation。你可以把关系直接理解成一张二维表比如 student 表就是一个关系course 表也是一个关系。关系有名字用来在表达式中引用。第二个是元组Tuple。元组是关系中的一行代表一条记录。第三个是属性Attribute。属性是关系中的一列代表这条记录的某个特征比如“学号”“姓名”“学院”。这三个概念几乎是一一对应到日常数据库术语的关系模型概念数据库表概念英文缩写说明关系表Relation一张有结构的数据集合元组行Tuple表中的一条记录属性列Attribute记录的一个字段度列数Degree关系中属性的个数基数行数Cardinality关系中元组的数量还有一个关键点在严格的关系模型中关系是一个集合。集合中的元素是元组而集合本身有两个性质元素之间无序并且不允许重复。换句话说理想的“关系”里不会出现两行完全一样的记录。这一点和实际 SQL 表是有差异的。普通 SQL 表是“多重集合”它允许重复行而关系代数里的“关系”不允许重复元组。这个差异会在投影运算中体现得非常明显关系代数默认投影结果会去重而 SQL 里的 SELECT 默认不去重必须显式写 SELECT DISTINCT 才能保证结果等价。这个坑后面我们会专门讲。我们下面会用学生选课数据库作为贯穿全文的例子。它包含三张表student学生、course课程、sc选课记录。对应关系模型就是三个关系它们的属性组合在一起构成一个小型数据库模式。3. 关系代数是什么一种以集合为对象的操作语言关系代数是一种过程化查询语言。说它是过程化的是因为它明确指出了如何从一个或多个关系得到结果关系先做什么操作后做什么操作都写在表达式里。它不是写给最终用户看的而是数据库理论和查询引擎之间的桥梁。关系代数的基本思想是对关系表施加一系列操作每个操作接收一个或两个关系作为输入输出一个新的关系。因为输入和输出都是关系所以可以把多个操作嵌套起来形成一个复杂表达式。关系代数中最常用的操作可以分成四类单表操作选择σ、投影π、重命名ρ。集合操作并∪、差−、交∩。连接操作笛卡尔积×、连接⋈、自然连接、外连接。扩展操作除÷、聚合等。本文第一部分聚焦前三类中的基础部分选择、投影、并、差、笛卡尔积和重命名。连接虽然会提到但它的详细规则更适合放在第二部分。关系代数有一个非常重要的性质叫做闭包性。所谓闭包就是操作结果仍然是一个关系。由于结果依然是关系你就可以继续对这个结果执行操作。比如你可以先对 student 做选择得到计算机学院的学生再对这个结果做投影只保留学号和姓名得到一个新的关系π_sno, sname ( σ_college计算机 (student) )整个表达式的结果仍然是一个关系后续还可以继续和其他关系做并、差、笛卡尔积。这个性质是关系代数能嵌套组合的基础也是它能成为查询语言理论核心的原因。4. 选择与投影单表查询的两个基本动作4.1 选择筛选行选择运算用希腊字母 σ 表示英文是 Selection但它并不是“选出某些列”而是“选出某些行”。它的作用是根据条件过滤关系中的元组等价于 SQL 里的 WHERE 子句。记法如下σ_条件(关系名)比如查询计算机学院的学生的全部信息σ_college计算机 (student)等价 SQL 是SELECT * FROM student WHERE college 计算机;这里“条件”可以是单个谓词也可以是复合条件用 ∧AND、∨OR、¬NOT组合。例如查询计算机学院并且年龄大于 20 的学生σ_college计算机 AND age20 (student)在关系代数中选择是“按行过滤”不会改变关系的属性结构。也就是说σ 的结果和输入关系有相同的属性列只是行数变少。4.2 投影筛选列投影运算用希腊字母 π 表示英文是 Projection。它负责选出某些属性列相当于 SQL 的 SELECT 列表。记法如下π_属性1, 属性2(关系名)比如查询所有学生的学号和姓名π_sno, sname (student)等价 SQL 是SELECT DISTINCT sno, sname FROM student;这里为什么要强调 DISTINCT因为关系代数是基于集合的它不允许重复元组。但在一个真实的学生表里学号是主键姓名和学号组合自然也不会重复。所以这个例子里加不加 DISTINCT 结果一样。如果换一个场景比如只查询学生所在学院关系代数的结果会自然去除重复学院而 SQL 的SELECT college FROM student会保留所有重复值你必须写SELECT DISTINCT college才能得到同样的效果。投影之后结果的属性数量一定不超过输入关系。所以如果你在 π 之后又想继续对结果做选择那么选择条件里只能使用投影后的属性。比如先投影学号和学院再选择学院为计算机可以写成σ_college计算机 ( π_sno, college (student) )但如果投影时没有保留 sname后面就不能选 sname因为它已经不在当前关系中了。这是写嵌套表达式时最容易被忽略的一点。4.3 选择与投影的顺序在实际写查询时选择与投影通常是组合出现的。那么先选择还是先投影关系代数表达式会不一样但结果可能相同。从优化角度看通常推荐“先选择再投影”。因为选择可以减少行数从而减少投影处理的元组数量。这个规则在数据库优化器里叫选择下推后面最佳实践部分还会提到。一个典型的组合查询是查询计算机学院学生的学号和姓名。π_sno, sname ( σ_college计算机 (student) )对应 SQLSELECT DISTINCT sno, sname FROM student WHERE college 计算机;从这个例子可以看出关系代数的嵌套方式对应到 SQL 中可能就是一句 WHERE 加 SELECT 列表。理解它的组合逻辑比死记 SQL 语法重要得多。5. 集合运算并、差、笛卡尔积与重命名5.1 并相容性关系代数里的并运算 ∪ 和数学中的集合并集含义一致把两个关系的元组合并去掉重复部分。但并不是任意两个关系都能做并运算它们必须满足并相容性。并相容性有两个条件两个关系的属性个数相同也就是度相同。对应位置属性的取值域兼容比如都是数值类型或者都是字符类型。这在 SQL 里同样对应 UNION 的要求两侧查询的列数必须相同且对应列类型兼容。例如查询软件学院和人工智能学院学生的学号π_sno( σ_college软件 (student) ) ∪ π_sno( σ_college人工智能 (student) )等价 SQLSELECT sno FROM student WHERE college 软件 UNION SELECT sno FROM student WHERE college 人工智能;注意SQL 的 UNION 默认会去重。如果想保留重复行就要用 UNION ALL。从关系代数角度看关系代数的 ∪ 是“集合的并”天然去重而 SQL 的包语义要求你主动选择。为了方便后续演示我们先准备一个完整的示例数据库。下面这段 SQL 会创建学生、课程、选课三张表并插入测试数据。CREATE DATABASE IF NOT EXISTS university_demo DEFAULT CHARACTER SET utf8mb4; USE university_demo; CREATE TABLE student ( sno CHAR(10) PRIMARY KEY, sname VARCHAR(20) NOT NULL, college VARCHAR(30), age INT ); CREATE TABLE course ( cno CHAR(6) PRIMARY KEY, cname VARCHAR(50) NOT NULL, credit INT ); CREATE TABLE sc ( sno CHAR(10), cno CHAR(6), score DECIMAL(5,1), semester VARCHAR(20), PRIMARY KEY (sno, cno) ); INSERT INTO student (sno, sname, college, age) VALUES (20210001, 张明, 计算机, 20), (20210002, 李华, 软件, 21), (20210003, 王芳, 人工智能, 19), (20210004, 陈晨, 计算机, 22); INSERT INTO course (cno, cname, credit) VALUES (C001, 数据库系统, 4), (C002, 操作系统, 4), (C003, 程序设计, 3); INSERT INTO sc (sno, cno, score, semester) VALUES (20210001, C001, 85.0, 2024-2025-1), (20210002, C001, 78.0, 2024-2025-1), (20210003, C002, 90.0, 2024-2025-1), (20210004, C002, 66.0, 2024-2025-1), (20210004, C001, 88.0, 2024-2025-1);5.2 集合差减去一个集合集合差用 − 表示作用是从第一个关系中删除第二个关系里也出现的元组。它对应 SQL 里的 EXCEPT 或 MINUS。不同的数据库语法不一样PostgreSQL 和 SQL Server 支持 EXCEPTOracle 用 MINUSMySQL 从 8.0.31 开始也加入了 EXCEPT 支持。一个典型场景是查询选了 C001 课程但没选 C002 课程的学生。关系代数π_sno( σ_cnoC001 (sc) ) − π_sno( σ_cnoC002 (sc) )等价 SQLSELECT DISTINCT sno FROM sc WHERE cno C001 EXCEPT SELECT DISTINCT sno FROM sc WHERE cno C002;对于较老版本的 MySQL可以改用 LEFT JOIN 实现同样的集合差逻辑SELECT DISTINCT s1.sno FROM sc s1 LEFT JOIN sc s2 ON s1.sno s2.sno AND s2.cno C002 WHERE s1.cno C001 AND s2.sno IS NULL;这个替代写法的思路是先取出选了 C001 的记录再左连接选课表把同一学生选了 C002 的记录拼接进来如果右侧没有匹配说明该学生没有选 C002此时右侧的 sno 就是 NULL。WHERE 里过滤掉右侧非 NULL 的行剩下就是“选了 C001 但没选 C002”的学生。5.3 笛卡尔积所有组合笛卡尔积用 × 表示也常被称为叉积。它把第一个关系的每个元组和第二个关系的每个元组组合成一行结果关系的属性数量是两个关系属性数之和元组数量是两个关系行数之积。例如student 有 4 行course 有 3 行student × course 的结果就有 12 行。对应的 SQL 是 CROSS JOINSELECT student.sno, student.sname, course.cno, course.cname FROM student CROSS JOIN course;笛卡尔积本身在业务查询中很少直接使用因为会产生大量无意义组合。比如“张明”会和“数据库系统”“操作系统”“程序设计”都组合一遍。但这并不代表它不重要——它是连接运算的基础。连接运算本质上就是“先做笛卡尔积再通过条件过滤掉不匹配的行”。理解了这一点后面学 JOIN 时会顺畅很多。5.4 重命名给关系换个名字重命名运算用 ρ 表示作用是给关系或属性起一个新名字常见于自连接场景。比如ρ_s( student )表示把 student 关系重命名为 s。在后面的关系中你可能想把 student 和它的另一个副本做连接例如查询“年龄比自己同学院某位同学大的学生”这时如果不做重命名就没办法在同一个表达式中区分两个 student 实例。重命名看起来简单却是关系代数表达式中容易被忽略的细节。尤其在需要用多个相同关系的场景里没有它表达式根本写不出来。6. 完整示例用关系代数表达“选了 A 课但没选 B 课”现在我们用一个完整案例把前面讲过的运算串起来。需求查询选了 C001 课程但没有选 C002 课程的学生的学号。第一步先把选课表 sc 中 C001 的记录拿出来这是选择。σ_cnoC001 (sc)得到所有选修 C001 的选课记录。第二步只保留学号这是投影。π_sno( σ_cnoC001 (sc) )得到一个学生学号集合。第三步用同样的方式取出选了 C002 的学号集合π_sno( σ_cnoC002 (sc) )第四步对两个集合做差π_sno( σ_cnoC001 (sc) ) − π_sno( σ_cnoC002 (sc) )这个关系代数表达式的含义非常清晰先找“选了 C001 的集合”再找“选了 C002 的集合”最后去掉交集部分。对应 SQL 可以直接翻译SELECT DISTINCT sno FROM sc WHERE cno C001 EXCEPT SELECT DISTINCT sno FROM sc WHERE cno C002;看SQL 几乎和关系代数一一对应。这也是为什么很多数据库课程会先教关系代数再教 SQL它能让你在写复杂查询时先想清楚集合层次再写语法。这个案例也展示了关系代数“过程化”的价值。如果你直接写 SQL可能会用 NOT IN、NOT EXISTS、LEFT JOIN 多种方案每一种都“能跑”但很难一眼看出逻辑。而关系代数把查询拆成了“选课 → 过滤 → 投影 → 集合差”四个步骤任何一步都不会含糊。7. 初学者最容易踩的四个坑问题现象可能原因排查方式解决方案SQL 查询返回重复行而关系代数结果却没有重复关系代数基于集合SQL 默认基于包去掉 SELECT DISTINCT 观察结果差异根据业务预期决定是否加 DISTINCT对投影结果继续做选择时报“字段不存在”π 之后只剩预期属性原表其他列不可见查看当前表达式阶段的所有属性先做选择再把所有需要的属性放进投影并运算报错列数不一致两个关系不满足并相容性检查两侧表达式的属性和类型补充常量列或显式类型转换用 NOT IN 做集合差结果意外为空子查询结果里包含 NULLNOT IN 遇到 NULL 返回未知检查子查询结果是否有 NULL改用 NOT EXISTS 或 LEFT JOIN IS NULL第一个坑和第二个坑在考试题和面试题里尤其常见。投影是“丢弃列”的操作一旦丢弃后续就无法继续使用。所以关系代数表达式的顺序会直接影响结果是否合法这也是和 SQL 很不一样的地方SQL 的 SELECT 列表虽然也在逻辑上晚于 WHERE 执行但它的语法结构让很多人忽略了“先 FROM、再 WHERE、最后 SELECT”的逻辑顺序。第三个坑是关于并相容性的。关系代数如果对属性数量不同的两个关系做并运算会直接成为一个无效表达式。这个规则在 SQL 的 UNION 中同样存在不少同学正是通过这种报错才理解“为什么 UNION 两侧列数必须一致”。第四个坑是 NULL 值语义。关系代数通常讨论的是没有 NULL 的理想关系但真实数据库充满 NULL。NOT IN 在遇到 NULL 时会有非常反直觉的行为这也是我建议在实际项目中优先用 NOT EXISTS 的原因之一。它能避免很多难以排查的结果异常。8. 工程建议用关系代数思维写出更靠谱的 SQL关系代数不直接写在业务代码里但它能改善你写 SQL 的方式。下面几个建议是我认为对日常开发最有帮助的几条。第一复杂查询先写关系代数草图。如果你要写一个嵌套很多层的查询先在纸上用 σ、π、∪、− 表示出集合层次再翻译成 SQL。不要小看这一步它能帮你把“用 JOIN 还是子查询”这种纠结变成“这里其实是在做集合差而不是连接”。第二理解选择下推和投影下推。在数据库执行计划里优化器通常会尽量把选择操作往树的下层移动。选择下推意味着“先减少行数再做其他操作”投影下推意味着“先减少列数再继续处理”。你自己手写 SQL 或调整查询时也可以遵循同样的原则能早过滤就早过滤不要一开始把所有列和所有行都查出来再处理。这不仅能减少数据库的 IO也能让查询意图更清晰。第三分清楚“集合语义”和“包语义”。关系代数是集合语义不允许重复元组SQL 是包语义默认保留重复行。当你从关系代数转换到 SQL 时要主动决定是否需要 DISTINCT而不是想当然认为两者结果一样。对于 UNION 和 UNION ALL同样要明确如果你要的是集合并用 UNION如果你只关心拼接数据且不介意重复用 UNION ALL性能通常更好。第四集合差要谨慎实现。MySQL 老版本没有 EXCEPT用 NOT IN 要格外小心 NULL。最稳妥的写法是 NOT EXISTS 或 LEFT JOIN IS NULL。在面试中如果写出 NOT IN面试官追问“子查询有 NULL 怎么办”很多候选人会卡住。能解释清楚这个边界本身就是一种加分项。第五给关系和属性起可读的名字。关系代数里的关系名尽量和业务含义一致不要用 a、b、c 这种无意义名称。如果做了重命名要在表达式里保持命名一致否则别人根本看不懂。9. 下一步连接运算与第二阶段到这里关系代数查询的第一部分就讲完了。你已经掌握了单表查询的两种核心操作——选择和投影也掌握了三种集合操作——并、差、笛卡尔积。这些运算合在一起已经能解决大量“基于集合”的查询问题。接下来的重点是连接运算。你会看到连接其实就是笛卡尔积加上选择条件的组合但数据库会通过索引、HASH JOIN、NESTED LOOP 等算法高效实现它而不是真的把所有组合都算出来。连接的类型包括 θ 连接、等值连接、自然连接、左外连接、右外连接和全外连接。掌握这些你才能真正理解业务查询里的 JOIN 为什么可以这样写又为什么有时会“算得很慢”。如果你在跟着 Neso Academy 或其他数据库系统公开课学习下一步可以重点看连接运算和除运算的推导。除运算在教材里经常用来做“查询选修了全部课程的学生”这类问题它比用 SQL 实现更直观也是理解分组语义的补充。学习关系代数时不必追求把所有符号都背下来更重要的是建立“查询就是关系运算序列”的意识。下次写复杂 SQL 之前试着先在脑里过一遍我现在是在做选择、投影还是集合差当你习惯了这种思考方式阅读理解执行计划、排查慢查询、设计数据库查询方案的能力都会比单纯背 SQL 语法要高一个层次。建议收藏本文把示例数据库在本地跑起来自己把表达式改一改再看看结果变化。实践一遍比看十遍公式都管用。