一、关系模型基础
1.1 关系模型的数据结构:二维表
关系模型的数据结构本质上就是一张二维表(2D table)。表中的每一行称为一个元组(Tuple),对应一个数据对象(如一个学生的记录);每一列称为一个属性(Attribute),如姓名、年龄、性别等。
行的别名:元组、记录;列的别名:属性、字段。这些名称在数据库领域中混用,含义相同。
元组的集合称为关系(Relation),即整张表。属性的个数称为元数(Degree),元组的个数称为基数(Cardinality)。
关系模式(Relation Schema)描述的是表的结构:包含哪些属性,每个属性的数据类型是什么。例如学生表包含学号(字符型)、姓名(字符型)、年龄(整型)、性别(字符型)——这五个属性的定义就构成了关系模式。
1.2 关系模型的数学基础
在关系数据库中,所有操作的数学基础是集合论(Set Theory)。每一个操作的输入是集合(或关系),输出也是集合。
1.3 关系模型的优点
- 数据结构简单:就是二维表,理解和使用门槛低。
- 数据独立性好:三层模式架构(物理模式、逻辑模式、子模式)使得修改下层不影响上层——例如修改文件的存储方式,用户完全感知不到。相比之下,网状数据库需要用户直接面对底层的图结构,复杂度高得多。
- 有严格的数学基础:基于集合论,操作的定义和性质可以严格推导。
- 与谓词逻辑结合紧密:在上一次人工智能热潮中,关系数据库与逻辑推理系统整合,形成了知识库(Knowledge Base)的技术体系——底层用关系数据库存储事实数据,上层通过规则系统进行推理。在一些确定性较强的行业中,专家系统仍然使用这种架构。当然,随着大模型和深度学习的出现,知识库也有了新的做法。
二、关键码与键
2.1 候选键(Candidate Key)
候选键是一个属性的集合,满足两个条件:
- 唯一决定:通过这些属性可以唯一地标识表中的每一条元组。即这些属性形成的值组合在整张表中是唯一的。
- 最小性:在这个属性集合中去掉任何一个属性,都不能再起到唯一决定的作用。
比如工号可以唯一决定一条元组,那么{工号}就是一个候选键。但如果用{工号,姓名},去掉姓名后工号仍然能唯一决定,所以{工号,姓名}就不满足最小性,不是候选键。
2.2 主键(Primary Key)
一张表中可能有多个候选键(如工号和身份证号都可以唯一标识一个人)。系统会从中挑选一个作为主键。选定主键后,数据库系统会自动在该键上建立索引,以提高查询效率。
主键一定是候选键之一。选择哪个候选键作为主键,由用户定义。
2.3 超键(Super Key)
有唯一决定作用但不是最小的属性集合称为超键(Super Key)。如{工号,姓名},去掉姓名后只用工号也能唯一决定,所以它是超键而非候选键。
2.4 外键(Foreign Key)
外键用于建立两张表之间的关联。例如有两张表:学生表 S(学号, 姓名, 年龄, 性别)和成绩表 SC(学号, 课程号, 成绩)。SC 中的学号是一个外键,它引用 S 表中的学号(主键)。
外键的约束规则:外键的取值只允许两种可能——空值或等于被参照关系中某个主键的值。即”不允许引用不存在的实体”。
学生选课成绩记录中的学号必须来自学生表;外键值必须是主表中已经存在的。
外键也可以在同一张表内自引用。例如课程表中有”先导课程”属性,它引用的也是课程表中的课程号——先导课程必须是课程表中已存在的课程。
主键-外键关系的命名:
- 被参照的关系(主键所在表)称为参照关系或主表
- 包含外键的关系称为依赖关系或子表/副表
三、完整性约束
3.1 实体完整性规则(Entity Integrity)
要求元组在组成主键的属性上不能有空值。因为一旦有空值,主键就无法起到唯一标识的作用(两个空值在逻辑上无法比较,可能被认为”相同”)。
3.2 参照完整性规则(Referential Integrity)
即外键约束:外键值要么为 NULL,要么等于被参照表中某个主键的值。这是关系模式中必须遵守的约束。
3.3 用户定义的完整性规则(User-Defined Integrity)
用户可以自定义对属性值的约束。例如,学生的年龄必须在 15 到 30 岁之间:
1
CHECK (age BETWEEN 15 AND 30)
如果插入一条年龄为 40 的记录,数据库系统会报错并拒绝插入。
3.4 为什么约束应该放在数据库层面
约束应该尽可能放在数据库上定义,而不是分散在各个应用程序里。
原因:数据库是共享的,支持多个用户和多个应用程序。如果约束写在应用程序里,每个应用程序都要实现一遍;当规则需要修改时(如教育部把年龄范围扩大了),就要在每一个应用程序中去改。但如果约束定义在数据库层面,只需修改一处即可。
四、关系模式的性质
-
属性值不可再分(Atomicity):每个属性值不能是集合或复合结构。例如一个人有两个手机号,不能说手机这一列放一个集合,可以拆成”手机1”和”手机2”两列。虽然某些数据库系统支持复杂数据类型(如 JSON),但使用时很多关系操作(如索引)会变得不方便。这也是后来 NoSQL 数据库出现的部分原因——关系模型在表达能力上的局限性。
-
不允许有重复元组:因为定义了键,键值唯一,就不可能有完全相同的两行。
-
元组无序:关系是一个集合,元组之间在逻辑上没有先后顺序。
-
属性无序:列的顺序在逻辑上也不重要。
五、子模式与三层模式
5.1 子模式(Subschema)
数据库中的逻辑模式定义了所有表的结构。但不同的用户有不同的关注点。例如:
- 本科教务人员关心学生的选课信息
- 研究生教务人员关心导师和论文信息
- 科研管理人员关心项目信息
子模式就是为每个用户定制的数据视图。比如教务人员只需要学号、姓名、课程号、成绩,不需要直接记忆学号,就可以建立一个包含 {学号, 姓名, 课程号, 成绩} 的子模式。
子模式在 SQL 中通过视图(View)来实现。
5.2 三层模式架构
1
2
3
4
5
用户1 ---- 视图1 ----┐
用户2 ---- 视图2 ----┤
用户3 ---- 视图3 ----┼── 基本表(逻辑模式)── 文件(物理模式)
│
└── 映射关系
三层模式的独立性:
- 修改物理存储(如将一个表从放在一个文件改为放在多个文件),只需修改物理模式与逻辑模式之间的映射
- 上层用户完全感知不到变化
5.3 存储模式(物理模式)
六、ER 模型向关系模型的转换(简要介绍)
- 每个实体映射为一个关系
- 一对一(1:1)联系:将联系的属性放到任意一边的表中(如将”任职年月”放到学校表或校长表中)
- 一对多(1:N)联系:将联系信息放到”多”的一边(如某系有多个教师,聘任信息放在教师表,不能放在系表——因为放到系表会变成集合,违反原子性)
- 多对多(M:N)联系:单独建一张表存放联系信息(如学生选课)
后面有专门的章节详细讲解。
七、关系代数(Relational Algebra)
7.1 为什么需要关系代数
SQL 语言是一种描述性语言——它只描述”你想要什么数据”,但不描述”怎么去访问数据”。数据存储在硬盘的文件中,SQL 语句本身不具备执行性。
关系代数位于 SQL 和物理文件访问之间,充当桥梁:
1
SQL语言 → 关系代数表达式 → 查询路径(可执行的程序) → 文件访问
关系代数中的每一个操作都有明确的执行语义,可以组合成程序。从 70 年代初提出关系模型到 80 年代真正形成数据库产品,这十年间数据库领域的人主要干的就是这件事——查询处理与查询优化(Query Processing & Query Optimization)。
7.2 集合表示法
在关系代数中,操作的结果用集合符号表示。例如:
\[R \cup S = \{t \mid t \in R \lor t \in S\}\]其中 $t$ 代表集合中的元组(Tuple),”$\mid$” 后面是元组需要满足的条件。
7.3 五种基本操作
(1)并(Union, $\cup$)
两个关系模式相同的表合并,去掉重复元组:
\[R \cup S = \{t \mid t \in R \lor t \in S\}\](2)差(Difference, $-$)
属于 R 但不属于 S 的元组:
\[R - S = \{t \mid t \in R \land t \notin S\}\](3)笛卡尔积(Cartesian Product, $\times$)
将两个关系中的元组两两拼接。若 R 有 m 条元组,S 有 n 条元组,则结果有 $m \times n$ 条:
\[R \times S = \{(t_R, t_S) \mid t_R \in R \land t_S \in S\}\]笛卡尔积把所有元组不做任何筛选地两两拼接,很多拼接(如 A 学生连上 B 学生的成绩)毫无意义。所以需要后面的连接操作来加约束。
(4)投影(Projection, $\pi$)
只保留部分属性(列),去掉其他属性:
\[\pi_{A_1, A_2, \ldots, A_k}(R) = \{t[A_1, A_2, \ldots, A_k] \mid t \in R\}\]- 可用属性名表示(如 $\pi_{C, A}$),也可以用数字表示列序号(如 $\pi_{3, 1}$)
- 投影后可能产生重复元组,需要去重
(5)选择(Selection, $\sigma$)
挑选出满足特定条件的元组(行):
\[\sigma_F(R) = \{t \mid t \in R \land F(t) = \text{true}\}\]如 $\sigma_{B > 3}(R)$ 表示从 R 中挑出第二个属性值大于 3 的元组。
选择 vs 投影:选择是”挑行”,投影是”挑列”。
7.4 组合操作
通过基本操作组合而成。
交(Intersection, $\cap$)
可用两个差操作实现:
\[R \cap S = R - (R - S)\]连接(Join)
$\theta$-连接:在笛卡尔积上加一个比较条件:
\[R \bowtie_{i \theta j} S = \{(t_R, t_S) \mid t_R \in R \land t_S \in S \land t_R[A_i] \;\theta\; t_S[A_j]\}\]其中 $\theta$ 可以是 $<, >, =, \leq, \geq, \neq$ 等比较运算符。
F-连接:条件可以是一个复合的布尔表达式(多个条件的 AND/OR)。
自然连接(Natural Join, $\bowtie$)
最常用的连接类型。步骤:
- 先做笛卡尔积
- 在公共属性上做选择(公共属性值相等)
- 投影去掉重复的公共属性列
自然连接本质上就是把同一个对象分散在不同表中的信息合并到一起。如果两个表的公共属性是键(如学号),则自然连接恰好将同属一个学生的所有信息拼在一起,而不会出现”张学生的信息连上李学生的成绩”这种无意义的情况。
7.5 关系代数的现代意义
大模型时代的新角色:一是支撑传统查询处理,二是作为 Text-to-Data 的中间层——自然语言 → 关系代数表达式 → 可执行程序,这比 Text-to-SQL 更容易实现(因为关系代数是函数调用式的,更接近编程语言,而 SQL 中有大量复杂逻辑)。虽然简单查询的 Text-to-SQL 做得不错,但复杂查询效果不佳,所以 Text-to-Relational-Algebra 是当前的一个研究方向。
八、SQL 语言概述
8.1 SQL 的历史
- 1970 年:关系模型提出
- 1971 年:IBM 开发了第一个关系数据库 System R,并设计了查询语言 SEQUEL
- 之后演变为 SQL(Structured Query Language,结构化查询语言)
- 形成了国际标准:SQL-92 等
所有关系数据库都遵循 SQL 语言标准。学了一个关系数据库的 SQL,再去学其他数据库(Oracle、MySQL、GaussDB)就会发现 SQL 部分基本一致。甚至到了 NoSQL 数据库,虽然查询语言略有不同,但思路是相通的。
不同数据库的差异主要体现在:
- 数据类型:不同数据库支持的数据类型略有差异
- 扩展功能:某些数据库会基于标准 SQL 增加独有的模块和功能
8.2 SQL 的组成
- 数据定义语言(DDL):CREATE、ALTER、DROP 等
- 数据查询语言(DML):SELECT
- 数据更新语言:INSERT、UPDATE、DELETE
- 嵌入式 SQL:在编程语言中使用 SQL
九、数据库模式与表定义
9.1 创建模式(CREATE SCHEMA)
模式类似于”姓”——标明数据库对象属于哪个用户。因为数据库是共享的,A 用户可能建了一张表 T,B 用户也建了一张表 T。为了区分,每张表前面加上模式名(即用户名)。例如:
1
CREATE SCHEMA a_test AUTHORIZATION A;
a_test是模式名AUTHORIZATION A表示由用户 A 创建,权限跟随 A- A 用户建的表 T,全名就是
a_test.T(或A.T)
9.2 删除模式(DROP SCHEMA)
1
2
3
DROP SCHEMA a_test CASCADE;
-- 或
DROP SCHEMA a_test RESTRICT;
这是两个重要选项,几乎所有和删除有关的 SQL 命令都有:
- CASCADE(级联式):比较”野蛮”,不管下面有没有子对象(如表),全部一起删除
- RESTRICT(约束式):比较”文明”,如果下面有子对象,则拒绝删除,提示你先去清理子对象
数据库的很多操作做完后就形成永久结果,无法恢复。如果不确定,尽量用 RESTRICT 而不是 CASCADE,避免误删重要数据。
9.3 创建表(CREATE TABLE)
1
2
3
4
5
6
7
CREATE TABLE S (
sno CHAR(4) NOT NULL,
sname CHAR(8) NOT NULL,
age INT,
gender CHAR(1),
PRIMARY KEY (sno)
);
NOT NULL:该列不允许为空UNIQUE:该列取值唯一PRIMARY KEY:定义主键,系统会自动在主键上构建索引,提高后续访问效率
9.4 修改表(ALTER TABLE)
1
2
3
4
ALTER TABLE S ADD advice CHAR(30); -- 新增列(不能带 NOT NULL)
ALTER TABLE S DROP advice CASCADE; -- 删除列
ALTER TABLE S MODIFY sno CHAR(6); -- 修改列的数据类型/长度
DROP TABLE S CASCADE; -- 删除整张表
新增列时不能带 NOT NULL 约束,因为新增列的初始值都是 NULL。
9.5 CHAR vs VARCHAR
| 类型 | CHAR(n) | VARCHAR(n) |
|---|---|---|
| 含义 | 定长字符串 | 变长字符串 |
| 存储 | 始终占用 n 个字符空间,不足用空格补齐 | 只占用实际字符数的空间 |
| 存储效率 | 较低(浪费空间) | 较高(回收未用空间) |
| 访问效率 | 较高(长度确定,定位快) | 略低(长度不固定) |
简言之:CHAR 偏重访问效率,VARCHAR 偏重存储效率。
9.6 数据类型一览
- 数值类型:整数、浮点数(不同精度)
- 字符串类型:CHAR、VARCHAR
- 位串类型(Bit String):01 串,如存储图片
- 时间类型:日期、时间、时间戳等
不同数据库支持的数据类型不同,建议查阅具体数据库的技术手册。
十、索引(Index)
10.1 为什么需要索引
工商银行可能有几十亿个账号全放在一个数据库里,但你卡插进去输完密码,很快就验证通过了。这是怎么做到的?——就是靠索引。
10.2 索引的原理
索引基于平衡树结构(在数据库中是 B+ 树,不是内存中的二叉搜索树,因为数据在硬盘上)。平衡树的性质:
- N 个节点,树高约为 $\log_k N$(k 为分支因子)
- 数据库中 k 可能大到 100,意味着 100 万条记录只需要比较 3 次
10.3 创建和删除索引
1
2
CREATE UNIQUE INDEX idx_sno ON S(sno);
DROP INDEX idx_sno;
UNIQUE表示该列上不能有重复值
10.4 索引的使用策略
系统管理员会监控数据库的慢查询(执行时间特别长的查询)。因为一个慢查询会锁住很多其他数据,造成整个系统性能下降。优化慢查询最主要的手段就是加索引——索引能让查询优化器找到更高效的执行方案。
但是,索引也不能乱加——加索引也有代价(占用存储空间,写操作时需要维护索引)。需要在实际应用中根据查询模式权衡。
十一、示例数据库
后续 SQL 查询将围绕以下三张示例表展开:
| 表名 | 含义 | 属性 |
|---|---|---|
| S | 学生表(Student) | sno(学号), sname(姓名), age(年龄), gender(性别) |
| SC | 选课表(Student-Course) | sno(学号), cno(课程号), grade(成绩) |
| C | 课程表(Course) | cno(课程号), cname(课程名), teacher(任课教师) |
- S 的主键是 sno
- SC 的主键是 (sno, cno),sno 是外键引用 S.sno,cno 是外键引用 C.cno
- C 的主键是 cno
十二、SQL 查询语句(SELECT)
12.1 基本结构
1
2
3
SELECT 列名1, 列名2, ...
FROM 表名1, 表名2, ...
WHERE 条件表达式;
三部分的含义:
- FROM:你要的数据涉及哪些表(先告诉 DBMS 去哪找)
- WHERE:这些数据需要满足什么条件(包括连接条件和筛选条件)
- SELECT:最终要返回哪些列
* 代表返回所有属性(列)。
12.2 与关系代数的对应
1
SELECT A1, A2 FROM R, S WHERE F;
等价于:
\[\pi_{A_1, A_2}(\sigma_F(R \times S))\]- FROM 对应笛卡尔积
- WHERE 对应选择(连接条件 + 筛选条件)
- SELECT 对应投影
SQL 语言的设计理念是:越简单越好,最好所有人都能学会。所以只需描述要什么数据即可。
12.3 WHERE 子句中的运算符
WHERE 子句中可以使用多种运算符:
- 算术运算符:+、-、*、/ 等
- 逻辑运算符:AND、OR、NOT
- 比较运算符:=、<>、<、>、<=、>=
- 集合运算符:IN、NOT IN、EXISTS、NOT EXISTS 等
- 其他运算符:BETWEEN、LIKE 等
- 聚合函数:COUNT、SUM、AVG、MAX、MIN 等(后面会专门讲)
- 嵌套子查询:WHERE 中的运算对象本身也可以是另一条 SELECT 语句(因为 SELECT 返回的是一个集合)
实际上,SQL 的 SELECT 语句可以表达所有关系代数的表达式。二者在表达能力上是等价的,这也是关系代数被用作 SQL 与物理存储之间桥梁的原因。
12.4 简单查询示例
例1:检索学习了课程号为 C2 的学生的学号和成绩。
1
2
3
SELECT sno, grade
FROM SC
WHERE cno = 'C2';
因为学号、课程号、成绩三个属性都在 SC 这一张表里,所以只需 FROM SC。
例2:检索学习了课程号为 C2 的学生的学号和姓名。
1
2
3
4
-- 方法一:连接写法
SELECT S.sno, sname
FROM S, SC
WHERE S.sno = SC.sno AND cno = 'C2';
姓名只在 S 表有,但课程号只在 SC 表有,所以需要连接两张表。连接条件是 S.sno = SC.sno。
1
2
3
4
5
6
-- 方法二:嵌套查询(IN)
SELECT sno, sname
FROM S
WHERE sno IN (
SELECT sno FROM SC WHERE cno = 'C2'
);
先在内层查询中找到所有学了 C2 的学生的学号集合,再在外层找到学号在这个集合中的学生。
1
2
3
4
5
6
7
-- 方法三:嵌套查询(EXISTS)
SELECT sno, sname
FROM S
WHERE EXISTS (
SELECT * FROM SC
WHERE SC.sno = S.sno AND cno = 'C2'
);
对 S 表中的每一条元组(从上到下扫描),检查 SC 表中是否存在一条记录——其学号与该学生的学号相同且课程号为 C2。EXISTS 只要内层查询结果不为空即为真。这种查询中涉及外层表 S 的 sno 在内层被引用的写法称为关联子查询(Correlated Subquery)。
12.5 SQL 写法的灵活性
SQL 语言非常灵活,同一个查询可以有很多种写法。对于成熟的数据库系统(如 Oracle、DB2),不同写法的执行效率差别不大(查询优化器足够强大)。但对于一些较新的国产数据库,查询优化器可能对嵌套查询的理解不够好,效率可能存在差异。建议在写 SQL 时避免过于复杂的嵌套写法。
12.6 复杂查询示例
例3:检索选修了课程号为 C2 或 C4 的学生的学号。
1
2
3
SELECT sno
FROM SC
WHERE cno = 'C2' OR cno = 'C4';
这是 OR 条件,直接在 WHERE 中用 OR 即可。即使一个学生同时选了 C2 和 C4,系统也会自动去重(因为 SELECT 返回的是一个集合)。
例4:检索至少选修了 C2 和 C4 的学生的学号。
如果简单写成 WHERE cno = 'C2' AND cno = 'C4' 是不行的——因为每一条元组的课程号只有一个值,不可能同时等于 C2 又等于 C4。这涉及到 SC 表中的两条不同元组(一条是选了 C2 的记录,一条是选了 C4 的记录)。
解决方法是自连接(Self-Join):
1
2
3
SELECT X.sno
FROM SC AS X, SC AS Y
WHERE X.sno = Y.sno AND X.cno = 'C2' AND Y.cno = 'C4';
当查询结果涉及同一张表中的两条或多条不同元组时,只能通过连接(特别是自连接)的方式来完成。不能直接在 WHERE 中用 AND 处理多条记录之间的关系。
例5:检索不学 C2 这门课程的学生的学号/姓名。
不能简单写 WHERE cno != 'C2'——因为一个学生可能选了好几门课,其中一门不是 C2(比如他选了 C4),他就符合 cno != 'C2',但实际上他也选了 C2。这又是一个”涉及两条元组”的情况。
正确的写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 方法一:NOT IN
SELECT sno, sname
FROM S
WHERE sno NOT IN (
SELECT sno FROM SC WHERE cno = 'C2'
);
-- 方法二:NOT EXISTS
SELECT sno, sname
FROM S
WHERE NOT EXISTS (
SELECT * FROM SC
WHERE SC.sno = S.sno AND cno = 'C2'
);
面对”否”和”不”(NOT/NOT EXISTS)的时候,以及涉及多条元组之间关系的时候,一定要特别小心。不能简单地直接处理。
例6:检索学习了全部课程的学生的姓名。
“全部课程”这个概念很难直接表达——覆盖所有课程这件事。
这里用到双重否定(Double Negation)的技巧:
“学了全部课程” = “不存在一门课程该学生没学”
1
2
3
4
5
6
7
8
9
SELECT sname
FROM S
WHERE NOT EXISTS (
SELECT * FROM C
WHERE NOT EXISTS (
SELECT * FROM SC
WHERE SC.sno = S.sno AND SC.cno = C.cno
)
);
解读:从 S 表中逐行扫描每一个学生,检查是否”不存在一门课程——在 SC 中找不到这个学生选这门课的记录”。如果对某个学生来说,不存在这样的课程,则意味着他选了所有的课程。
例7:检索学了 S3 所学全部课程的学生的学号。
同样用双重否定:
1
2
3
4
5
6
7
8
9
10
SELECT DISTINCT sno
FROM SC AS X
WHERE NOT EXISTS (
SELECT * FROM SC AS Y
WHERE Y.sno = 'S3'
AND NOT EXISTS (
SELECT * FROM SC AS Z
WHERE Z.sno = X.sno AND Z.cno = Y.cno
)
);
对于每个学生 X,检查”不存在 S3 选的一门课,而 X 没选这课”。如果不存在这样的情况,则 X 覆盖了 S3 的所有课程。
总结:当 SQL 中需要表达”全部”“所有”这样的全称量词概念时,通常用双重否定(
NOT EXISTS+NOT EXISTS)来实现。这是一种重要的 SQL 编程思维。
十三、QBE 图示法(Query By Example)
13.1 背景
SQL 语言虽然比编程简单,但对于非计算机专业人员来说仍然过于复杂。数据库发展史上曾有一段时间,研究者希望设计一种”用图来表达查询”的语言,让普通用户也能使用。
13.2 基本思路
QBE 以一个表格的形式呈现,用户在对应列下面填入想要表达的信息:
- P.(Print):表示要输出该列
- 具体值(如
C2):表示筛选条件 - 下划线变量(如
_X):表示连接条件——同一变量在两处出现,代表这两处的值应相等 - NOT / 否定标记:表示”不存在”
例如,查询课程号为 C2 的学号和成绩,只需在表格中:
- 课程号列下填
C2 - 学号列下填
P. - 成绩列下填
P.
简单查询用 QBE 表达得很好,但复杂查询(如双重否定)在图中也会变得复杂。QBE 代表了数据库发展过程中一个重要的探索方向——让用户以更简单的方式表达查询需求。