关系模型敢叫「关系」,是因为它真的建立在数学的集合论上,本章开篇就把这层底牌亮出来。设两个域:学号域 D1 = {S001, S002},课程域 D2 = {C1, C2}。笛卡尔积 D1 × D2 是所有配对:
(S001,C1) (S001,C2) (S002,C1) (S002,C2) ← 2×2 = 4 种组合
而一张真实的选课表:
┌──────┬──────┐
│ 学号 │ 课程 │
├──────┼──────┤
│ S001 │ C1 │
│ S002 │ C2 │
└──────┴──────┘
只是上述 4 种组合的一个子集(选了 2 种)。所谓关系,就是笛卡尔积的一个子集;所谓表,不过是这个集合的展示形式。这不是文字游戏:子集意味着「不重复、无序」,集合的运算(并、交、差、笛卡尔积)全部适用,于是查询语言可以建立在严格的代数上,优化器才有定理可依。本章把这套语言学到能读会算,SQL 的每种语法在它那里都有原型。
先统一词汇,教材和工程黑话的对应关系值得专门列一遍:
| 代数术语 | 日常说法 | 例子 |
|---|---|---|
| 关系 Relation | 表 | Student |
| 元组 Tuple | 行 | (S001, 张三, CS) |
| 属性 Attribute | 列 | 学号 |
| 域 Domain | 这一列的合法取值集合 | 所有合法学号 |
| 关系模式 | 表结构 | Student(学号, 姓名, 系) |
三条集合论带来的性质,SQL 里各有对应物:元组不重复(SQL 靠主键约束逼近)、行无序(SQL 没有「第 5 行」这个概念,想有序必须 ORDER BY)、列无序(靠名字引用,不靠位置)。
码(key) 是本章的重头戏。候选码:能唯一标识元组且无冗余的属性组,「无冗余」三个字最容易被坑:{学号} 是候选码,{学号, 姓名} 也能唯一标识,但扔掉姓名还成立,所以它有冗余,不算。主码:从候选码里挑一个当正式身份,不许为空(没有身份的人没法被引用)。外码:本表的某属性组,引用另一张表的码,值必须来自对方(或者为空)。
Student(学号[主码], 姓名, 系编号)
Dept(系编号[主码], 系名)
SC(学号[外码→Student], 课程号[外码→Course], 成绩)
注意 SC 这张表:它自己没有天然的「身份列」,两个外码组合起来 (学号, 课程号) 才是主码,这种由多个外码拼成的码,是联系表的常态。
约束(constraint)是声明给 DBMS 的规矩,由它强制执行,谁也绕不过(包括你的 bug):
用一个真实的 SQLite 会话看约束怎么拦截脏数据:
sqlite> CREATE TABLE sc (学号 TEXT, 课程号 TEXT, 成绩 REAL,
...> PRIMARY KEY (学号, 课程号),
...> FOREIGN KEY (课程号) REFERENCES course(课程号));
sqlite> INSERT INTO sc VALUES ('S001', 'C9', 88);
-- 外码约束开启时报错: FOREIGN KEY constraint failed
工程忠告:约束写在数据库里,永远比写在应用代码里可靠。应用有十个入口(网页、脚本、内部工具),代码校验要写十遍还难免漏,数据库约束只写一遍、对所有人生效。见过不少团队图省事关掉外键(为了导入方便),后来用人工核对数据付出了十倍代价。
关系代数是一组「输入关系、输出关系」的运算符。用两张有数据的表当例子:
Student SC
学号 姓名 系 学号 课程 成绩
S001 张三 CS S001 C1 88
S002 李四 EE S001 C2 92
S002 C1 75
五个常用运算符,每个配一句人话和一个例子:
选择 σ(挑行):按条件留下部分元组。 得到张三那一行。对应 SQL 的 WHERE。
投影 π(挑列):留下部分属性,重复元组顺手去掉(集合语义)。 = {张三, 李四}。对应 SELECT 姓名,而 SQL 默认不去重,是多重集语义,要 DISTINCT 才回到集合,这是代数和 SQL 的第一处分叉。
连接 ⋈(拼表):最常用也最贵。 按同名属性(学号)相等拼成宽表:
S001 张三 CS S001 C1 88
S001 张三 CS S001 C2 92
S002 李四 EE S002 C1 75
定义上等于「笛卡尔积再按条件选择」,但实现上优化器绝不会真做笛卡尔积(第 9 章讲 hash join、nested loop 就是在这一步做文章)。
并 ∪ / 交 ∩ / 差 −:集合运算,要求两个关系结构相容(同列同域)。SELECT ... UNION 的原型。
除 ÷:最抽象也最有趣的一个,专门回答「对全部……都……」类问题。 找「选了所有课程的学生」。直觉:从 SC 里找那些「与任何课程配对都存在」的学号。SQL 里没有 ÷ 关键字,要用「双重否定」改写(NOT EXISTS (某课程他没选)),这句写法是 SQL 面试的经典题,根源就在这个除法。
| 关系代数 | SQL |
|---|---|
| σ条件(R) | SELECT ... WHERE 条件 |
| π属性(R) | SELECT 列 (DISTINCT 才是纯投影) |
| R ⋈ S | FROM R JOIN S ON ... |
| R ÷ S | NOT EXISTS 嵌套(双重否定) |
| R ∪ S / R − S | UNION / EXCEPT |
SQL 的执行语义基本就是「代数式的从里到外求值」,所以读复杂 SQL 的正确姿势是把它翻译回代数树:先谁和谁连接、再过滤什么、最后投影什么。第 9 章的查询优化,就是把同一个代数树变换成代价更低的等价树(谓词下推、连接重排),那时你会感谢今天学的这套记号。
UNION ALL 保留重复,UNION 去重;想要代数语义,记得 DISTINCT。{学号, 系编号} 若学号已唯一,它就不是候选码;判断标准是「去掉任何一个属性就不再唯一」。FOREIGN KEY 加上之后,悬空引用在插入时就会被拦截,这正是它存在的意义。{C1, C2} 构成的关系)并验证。PRAGMA foreign_keys=ON 打开约束重试。