跳至内容

Ch-2 关系模型

1. 关系结构

1.1 关系定义

关系模型 (Relational Model) 用关系表示数据。关系在直观上就是一张表,表中的列是属性,表中的行是元组。

形式化地,若 \(D_1,D_2,\cdots,D_n\) 是若干域,则关系 \(r\) 是这些域的笛卡尔积的一个子集:

\[ r\subseteq D_1\times D_2\times\cdots\times D_n \]

因此,关系中的每个元素都是一个 \(n\) 元组:

\[ (a_1,a_2,\cdots,a_n),\quad a_i\in D_i \]

例如 instructor 可以由 IDnamedept_namesalary 等属性组成;每一行对应一位教师的记录。

1.2 模式实例

关系模式 (Relation Schema) 描述关系的结构。

若属性为 \(A_1,A_2,\cdots,A_n\),关系模式可写为 \(R=(A_1,A_2,\cdots,A_n)\)。

在具体数据库中,常把关系名和属性列表写在一起,例如:

instructor(ID, name, dept_name, salary)

关系实例 (Relation Instance) 是某一时刻关系中实际包含的元组集合。

如果关系实例定义在模式 \(R\) 上,通常记为 \(r(R)\)。

模式相对稳定,实例会随插入、删除和更新变化。类似地,数据库模式 (Database Schema) 是整个数据库的逻辑结构,数据库实例 (Database Instance) 是某一时刻数据库中的实际内容。

1.3 域与空值

域 (Domain) 是某个属性允许出现的值集合。例如 salary 的域可以是合法工资数值集合,dept_name 的域可以是合法院系名称集合。

经典关系模型要求属性值是 原子值 (Atomic Value),也就是不可再分的单个值。这一要求使得关系操作可以把每个属性当作单独的比较和投影单位。

Note

现代数据库中可以使用数组、JSON、嵌套记录等复杂类型,但在关系模型的基本理论中,通常先假定属性值是原子的。

null 是一个特殊值,可出现在任意域中,用于表示未知、不存在或尚未填写。它会让比较、连接和聚集的语义变复杂,因此后续需要三值逻辑来处理。

1.4 无序性质

关系是元组的集合,因此元组顺序没有语义。数据库可以按任意物理顺序存储元组,查询结果如果没有显式 order by,也不应该依赖某种固定顺序。

属性通常也通过名称引用,而不是通过位置引用。实际 SQL 表虽然有列顺序,但查询语义主要依赖属性名。

2. 键与约束

2.1 超键候选键

超键 (Superkey) 是能够唯一标识关系中元组的一组属性。

设 \(K\subseteq R\)。

若任意两个元组在 \(K\) 上取值相同就必然是同一个元组,则 \(K\) 是 \(R\) 的超键:

\[ t_1[K]=t_2[K]\Rightarrow t_1=t_2 \]

超键不要求最小。例如在 instructor 中,ID 可以唯一标识教师,那么 {ID, name} 也能唯一标识教师,但它包含了多余属性。

候选键 (Candidate Key) 是最小的超键。最小的意思是不能再删去任何属性,否则就不再能唯一标识元组。

主键 (Primary Key) 是从候选键中选出的一个键,用作关系中元组的主要标识。主键应该稳定、简洁,并且不应为空。

2.2 外键约束

外键 (Foreign Key) 描述两个关系之间的引用关系。

若关系 \(r_1\) 的属性 \(A\) 引用关系 \(r_2\) 的主键 \(B\),则引用值必须能在被引用关系中找到。

更具体地说,\(r_1\) 中每个非空 \(A\) 值都应出现在 \(r_2\) 的 \(B\) 中。

这个约束可以写成:

\[ \forall t_1\in r_1,\ \exists t_2\in r_2,\ t_1[A]=t_2[B] \]

其中 \(r_1\) 称为 引用关系 (Referencing Relation)

其中 \(r_2\) 称为 被引用关系 (Referenced Relation)

参照完整性 (Referential Integrity) 要求引用不能指向不存在的元组。例如 teaches.ID 引用 instructor.ID 时,任意授课记录中的教师编号都必须能在教师关系中找到。

2.3 模式图

模式图把关系、主键、外键和参照完整性画在一起,便于理解整个数据库的结构。大学数据库中,studentinstructorcoursesectiontakesteaches 等关系通过主键和外键连接起来。

图中下划线属性表示主键。箭头从引用关系指向被引用关系,表示外键和参照完整性约束。

3. 基本代数

3.1 查询语言

关系查询语言可以分为 过程式 (Procedural)非过程式 (Non-Procedural)。关系代数属于过程式语言,因为它不仅说明要什么结果,也说明通过哪些操作一步步得到结果。

三类经典纯查询语言是:

语言特点
关系代数用操作组合表达查询过程
元组关系演算用元组变量和谓词描述结果
域关系演算用属性域变量和谓词描述结果

这三类语言在表达能力上等价。它们不是图灵完备语言,而是专门用于表达关系查询。

3.2 六个基本操作

关系代数 (Relational Algebra) 的操作以关系为输入,并产生新的关系作为输出。六个基本操作如下:

操作记号含义
选择\(\sigma\)保留满足谓词的行
投影\(\Pi\)保留指定属性并去重
\(\cup\)合并两个兼容关系
\(-\)保留只在左侧出现的元组
笛卡尔积\(\times\)两个关系的元组两两拼接
重命名\(\rho\)给关系或属性改名

选择操作保留满足谓词 \(P\) 的元组:

\[ \sigma_P(r)=\{t\mid t\in r\land P(t)\} \]

投影操作保留指定属性。由于关系是集合,投影后产生的重复元组会被去除:

\[ \Pi_{A_1,A_2,\cdots,A_k}(r) \]

并、差要求两个输入关系 兼容 (Compatible):属性个数相同,并且对应属性的域兼容。

笛卡尔积把两个关系中的元组两两拼接,若 \(r\) 和 \(s\) 的属性名冲突,需要先重命名:

\[ r\times s=\{tq\mid t\in r\land q\in s\} \]

重命名用于给中间结果命名,或在同一个关系参与多次运算时区分副本:

\[ \rho_x(E) \]

3.3 组合查询

关系代数表达式可以嵌套组合。例如查询物理系教师讲授过的课程编号,可以先选择物理系教师,再与 teaches 做连接条件筛选,最后投影所需属性。

一种写法是先做笛卡尔积,再用选择表达连接条件:

\[ \Pi_{\text{instructor.name},\text{course_id}}(\sigma_{\text{dept_name}=\text{"Physics"}\land \text{instructor.ID}=\text{teaches.ID}}(\text{instructor}\times \text{teaches})) \]

也可以先把 instructor 中物理系教师过滤出来,再与 teaches 组合。后者通常中间结果更小:

\[ \Pi_{\text{instructor.name},\text{course_id}}(\sigma_{\text{instructor.ID}=\text{teaches.ID}}(\sigma_{\text{dept_name}=\text{"Physics"}}(\text{instructor})\times \text{teaches})) \]

查询大学中最高工资也可以只用基本操作表达。先找出所有不是最高工资的工资值:

\[ \Pi_{\text{instructor.salary}}(\sigma_{\text{instructor.salary}<\text{d.salary}}(\text{instructor}\times\rho_d(\text{instructor}))) \]

再用所有工资减去这些非最高工资,得到最高工资:

\[ \Pi_{\text{salary}}(\text{instructor})-\Pi_{\text{instructor.salary}}(\sigma_{\text{instructor.salary}<\text{d.salary}}(\text{instructor}\times\rho_d(\text{instructor}))) \]

3.4 形式定义

关系代数的基本表达式可以是数据库中的一个关系,也可以是一个常量关系。

若 \(E_1\) 和 \(E_2\) 是关系代数表达式,则以下表达式仍然是关系代数表达式:

类型表达式
集合操作\(E_1\cup E_2\)
集合差\(E_1-E_2\)
笛卡尔积\(E_1\times E_2\)
选择\(\sigma_P(E_1)\)
投影\(\Pi_S(E_1)\)
重命名\(\rho_x(E_1)\)

这个封闭性很重要:每一步操作的结果仍是关系,因此可以继续作为下一步操作的输入。

4. 扩展运算

4.1 交与连接

扩展运算不会增加关系代数的表达能力,但能让常见查询更简洁。

交 (Set Intersection) 保留两个关系中都出现的元组:

\[ r\cap s=\{t\mid t\in r\land t\in s\} \]

交可以用差表示:

\[ r\cap s=r-(r-s) \]

自然连接 (Natural Join) 会在两个关系的同名属性上做相等匹配,并且同名属性只保留一份。

设 \(r(R)\) 和 \(s(S)\) 是两个关系,自然连接的结果模式是 \(R\cup S\)。

如果 \(R=(A,B,C,D)\),且 \(S=(E,B,D)\)。

则公共属性是 \(B\) 和 \(D\)。

自然连接可以写成:

\[ r\bowtie s=\Pi_{r.A,r.B,r.C,r.D,s.E}(\sigma_{r.B=s.B\land r.D=s.D}(r\times s)) \]

条件连接 (Theta Join) 把笛卡尔积和选择合并成一个操作:

\[ r\bowtie_{\theta}s=\sigma_{\theta}(r\times s) \]

自然连接满足交换律和结合律。若忽略属性显示顺序,\(r\bowtie s\) 与 \(s\bowtie r\) 等价。

4.2 外连接

普通连接只保留匹配成功的元组,未匹配的元组会丢失。外连接 (Outer Join) 在连接结果之外,把某一侧或两侧未匹配的元组补回,并用 null 填充缺失属性。

类型记号保留内容
左外连接\(r\mathbin{\text{⟕}}s\)保留左侧所有元组
右外连接\(r\mathbin{\text{⟖}}s\)保留右侧所有元组
全外连接\(r\mathbin{\text{⟗}}s\)保留两侧所有元组

左外连接可以理解为自然连接结果加上左侧未匹配元组:

\[ r\mathbin{\text{⟕}}s=(r\bowtie s)\cup((r-\Pi_R(r\bowtie s))\times\{(\text{null},\cdots,\text{null})\}) \]

右外连接对称地补回右侧未匹配元组:

\[ r\mathbin{\text{⟖}}s=(r\bowtie s)\cup(\{(\text{null},\cdots,\text{null})\}\times(s-\Pi_S(r\bowtie s))) \]

全外连接同时保留两侧未匹配元组:

\[ r\mathbin{\text{⟗}}s=(r\mathbin{\text{⟕}}s)\cup(r\mathbin{\text{⟖}}s) \]

4.3 半连接

半连接 (Semijoin) 只保留左侧中至少能在右侧找到匹配的元组,不把右侧属性拼接进结果。

半连接可以由条件连接和投影定义:

\[ r\ltimes_{\theta}s=\Pi_R(r\bowtie_{\theta}s) \]

例如相关子查询中的 exists 常可改写为半连接。查询 2022 年授课教师姓名可表示为:

\[ \Pi_{\text{name}}(\text{instructor}\ltimes_{\text{instructor.ID}=\text{teaches.ID}}\sigma_{\text{teaches.year}=2022}(\text{teaches})) \]

半连接的重点是“判断存在匹配”,而不是把两侧属性完整拼接出来。

4.4 空值逻辑

涉及 null 的比较不会得到普通的 true 或 false,而会得到 unknown。SQL 因此采用三值逻辑。

表达式结果
unknown or truetrue
unknown or falseunknown
unknown and trueunknown
unknown and falsefalse
not unknownunknown

where 条件中,结果为 unknown 的元组不会被选中。若要判断谓词本身是否为 unknown,可以使用 is unknown

4.5 赋值除法

赋值 (Assignment) 用于把复杂查询拆成多个临时关系。它不增加表达能力,但能让推导更清晰。

除法 (Division) 用于表达“对所有”的查询。

给定 \(r(R)\) 和 \(s(S)\),并且 \(S\subset R\)。

\(r\div s\) 是最大的关系 \(t(R-S)\),使得:

\[ t\times s\subseteq r \]

换句话说,结果中的每个 \(t\) 都必须能和 \(s\) 中每个元组组合,并且组合结果都出现在 \(r\) 中。

例如设 \(r(\text{ID},\text{course_id})\) 表示学生选过的课程。

设 \(s(\text{course_id})\) 表示生物系开设的所有课程。

则 \(r\div s\) 得到选过所有生物系课程的学生。

除法可用基本操作表示。先取所有可能的左侧候选:

\[ \text{temp}_1\leftarrow \Pi_{R-S}(r) \]

再找出那些缺少某个 \(s\) 元组匹配的候选:

\[ \text{temp}_2\leftarrow \Pi_{R-S}((\text{temp}_1\times s)-\Pi_{R-S,S}(r)) \]

最后从候选中删去不合格者:

\[ r\div s=\text{temp}_1-\text{temp}_2 \]

5. SQL 语义

5.1 广义投影

广义投影 (Generalized Projection) 允许在投影列表中使用算术表达式,而不仅是属性名:

\[ \Pi_{F_1,F_2,\cdots,F_n}(E) \]

其中 \(F_i\) 可以由常量和 \(E\) 中的属性组成。

例如 salary 表示年薪时,可以投影出月薪:

\[ \Pi_{\text{ID},\text{name},\text{dept_name},\text{salary}/12}(\text{instructor}) \]

5.2 聚集操作

聚集函数 (Aggregate Function) 接收一组值并返回一个值。常见聚集函数包括 avgminmaxsumcount

聚集操作的一般形式是:

\[ _{G_1,G_2,\cdots,G_n}\mathcal{G}_{F_1(A_1),F_2(A_2),\cdots,F_m(A_m)}(E) \]

其中 \(G_1,G_2,\cdots,G_n\) 是分组属性,可以为空。

其中 \(F_i(A_i)\) 表示对属性 \(A_i\) 使用聚集函数 \(F_i\)。

按院系求平均工资可以写成:

\[ _{\text{dept_name}}\mathcal{G}_{\text{avg}(\text{salary})}(\text{instructor}) \]

聚集结果默认没有属性名,实际使用时常通过重命名给它命名:

\[ _{\text{dept_name}}\mathcal{G}_{\text{avg}(\text{salary})\ \text{as}\ \text{avg_sal}}(\text{instructor}) \]

5.3 数据修改

数据库内容可以通过删除、插入和更新修改。这些操作也能用赋值思想表达。

操作关系代数表达
删除满足 \(P\) 的元组\(r\leftarrow r-\sigma_P(r)\)
插入关系 \(E\) 的结果\(r\leftarrow r\cup E\)
更新属性值删除旧元组后插入新元组

关系代数本身主要用于查询表达;数据修改语句需要额外考虑约束检查、触发器、并发控制和日志恢复。

5.4 多重集语义

纯关系代数使用集合语义,会自动去除重复元组。SQL 默认使用 多重集 (Multiset) 语义,除非显式使用 distinct,否则会保留重复行。

多重集语义下,设元组 \(t\) 在 \(r\) 中出现 \(m\) 次。

再设同一元组在 \(s\) 中出现 \(n\) 次。

集合操作的重复次数如下:

操作重复次数
\(m+n\)
\(\min(m,n)\)
\(\max(0,m-n)\)

选择操作保留输入中的重复次数,只要元组满足谓词。

投影操作对每个输入元组产生一个输出元组,因此可能保留重复投影结果。

笛卡尔积中,若左侧元组出现 \(m\) 次、右侧元组出现 \(n\) 次,拼接结果出现 \(mn\) 次。

5.5 SQL 对应

不含聚集的 SQL 查询可以看作选择、投影和笛卡尔积的组合:

select A1, A2, ..., An
from r1, r2, ..., rm
where P;

它对应的多重集关系代数表达式是:

\[ \Pi_{A_1,A_2,\cdots,A_n}(\sigma_P(r_1\times r_2\times\cdots\times r_m)) \]

带分组聚集的 SQL 查询可以先做笛卡尔积和选择,再按分组属性聚集:

select A1, A2, sum(A3)
from r1, r2, ..., rm
where P
group by A1, A2;

它对应的关系代数表达式是:

\[ _{A_1,A_2}\mathcal{G}_{\text{sum}(A_3)}(\sigma_P(r_1\times r_2\times\cdots\times r_m)) \]

如果 select 中只输出部分分组属性,则可先按完整 group by 属性聚集,再对最终结果投影。