26473 字
132 分钟
【数据库】基础回顾专题

前言#

本章重新回顾了数据库中的基本知识,包含ER图、关系模型、SQL、约束关系和事务等,并通过一些例题来进一步理解。

本章基于 “Fundamentals of Database Systems 6/7th” (《数据库系统基础 第6/7版》) 和一些常见的数据库面试八股文。笔记内容仅为个人见解,加入了一些可能不正式甚至不正确的个人见解,请勿过分解读,若存在异议,欢迎与我交流。

NOTE

资料:

下面是例题跳转链接:

ER Diagram#

ER图(Entity-Relationship Diagram)是一种用于描述实体和实体之间的关系的图表,常用于数据库设计。ER图通常包含实体(Entity),属性(Attribute),关系(Relationship)。

Definition
  • 实体(Entity):实体是数据库中的对象,例如用户、图书、订单等。实体集 (Entity Set) 是具有相同属性的实体的集合。
  • 属性(Attribute):属性是实体的描述,例如用户名、密码、邮箱等。
  • 联系(Relationship):联系是实体与实体之间的关系,例如用户和订单之间的关系。联系集 (Relationship Set) 是多个实体之间的联系的集合。

在一个最简单的ER图里,实体集使用 矩形框 表示,属性使用 椭圆/圆形 表示,联系集使用 菱形 表示。下面是一个简单的ER图:

其中,有一些关键的概念:

  1. 实体的码 / 键 (Key) 是 能区分每一个实体的属性集合,码一般分为:候选码(Candidate Key) 、主码 (Primary Key) 和外码 (Foreign Key)
Definition
  • 候选码 (Candidate Key):候选码是能 确定一个实体的属性的集合,通常由一个或多个属性组成。候补码可以重复,但至少有一个属性不能为空。例如对于员工表中,候选码可以是 [EmployeeID, {EmployeeName, EmployeeEmail}] 表示一个员工可以由员工编号EmployeeID 或 员工名字和邮箱的组合{EmployeeName, EmployeeEmail} 来唯一确定。
  • 主码 (Primary Key):主码是从候选码中唯一人为选择的属性,用于唯一标识一个实体集中的每一个元组 (Turple). 一个实体集中只能有一个主码,但可以有多个候选码。在 ER 图中,主码以 下划线 的形式表示。一条联系一定由实体的主码唯一确定,与联系的属性无关。
  • 外码 (Foreign Key):当一个是实体集中没有足够的属性去唯一标识一个实体,即它需要由其他的实体集中的属性的组合来唯一标识一个实体。外码通常用于表示关系。例如:大学课程信息保存在实体集 Course 中,它拥有属性 CourseIDCourseNameCourseCredits等。教授信息存在实体集 Professor 中,它拥有属性 ProfessorIDProfessorNameProfessorEmail等。学校规定,一节课由课程编号和教师编号唯一确定:{CourseID, ProfessorID}。那么,对于实体集 Course 而言,{CourseID, ProfessorID} 是它的主码,ProfessorID 是外码。
  1. 联系集约束 (Constraint): 实体参与联系时需要满足的一些条件,表示实体与联系的关系。

关系类型的 (Degree) 是指参与该联系集的实体数量。

  • Degree = 1 时,参与联系集的实体只有一个,称为 自反关系 (Recursive Relationship)。例如,员工中,下级需要向上级汇报,于是 Employee 存在自反关系 Report to,有两条线 (关系) 连接 Employee,因为上下级均为员工。
  • Degree = 2 时,此时的联系集称为 二元关系 (Binary Relationship)。

对于最常见的二元关系而言,基数比 (cardinality ratio) 指定了一个实体可以参与的关系实例的最大数量。对于实体集 ABab 表示 A 中的一个实体和 B 中的一个实体。可能的基数比如下:

  • 1:1 (One-to-One):表示 A 中的实体与 B 中的实体一一对应。例如:一个管理者最多只能管理一个部门,一个部门最多只能有一位管理者。
  • 1:N (One-to-Many):表示 B 中的实体 至多A 中的一个实体关联。例如:一个孩子 (b) 最多有一个母亲 (a),而一个母亲 (a) 可以有多个孩子 (b)。
  • N:1 (Many-to-One):表示 A 中的实体 至多B 中的一个实体关联。例如:一个员工 (a) 只能属于一个部门 (b),一个部门 (b) 可以有多个员工 (a)。
  • M:N (Many-to-Many):表示 A 中的实体可以对应0个或多个 B 中的实体关联,反之亦然。例如:一个员工 (a) 可以参与多个项目 (b),一个项目 (b) 也可以有多个员工 (a) 参与。

我们把二元关系的基数比又称为 码约束 (Key Constraint)。

此外,参与约束 (Participation Constraint) 指定了每个实体可以参与关系实例的 最小数量。如果实体集 A 中每一个实体 a 都参与联系集 R 的实例,则称 A 参与 R完全参与 (Total Participate)。例如:所有的学生 Student 都有选课 Course,则 Student 完全参与 Course

完全参与在 ER 图中一般以 双线 标记。

  1. 弱实体集 (Weak Entity Set): 没有足够的属性以形成主码的实体集,称为弱实体集。在 ER 图中以用 双线矩形框和双线菱形框 来标记弱实体集及其标识关系。

例如:员工的保险 Dependent 需要依赖员工 EmployeeEmployeeID 和保险的DependentID 才能唯一确定一个保险,此时,Dependent 是弱实体集。Dependent 中的所有实体,都依赖对应 Employee 的一个员工,所以 Dependent 参与关系时是完全参与,主码约束为 Employee : Dependent = 1 : N.

此时,弱实体集中的属性 DependentID 为部分码 (Partial Key),在 ER 图中用 虚下划线 表示。


ER 图总结与例题#

下面的图片总结了 ER 图中的重要元素的图形表示:

下面是一些设计 ER 图的例子:

  1. 我们希望为学校创建一个非常简单的数据库,用于记录教授、学生和课程的相关信息。

  • 每位教授需存储其教员编号、姓名和办公室编号。
  • 每位学生需存储其学号和姓名。
  • 每门课程需存储其课程编号(例如 CS240)和名称。
  • 每门课程仅由一位教授授课。
  • 每位学生必须至少选修一门课程。
  • 需为学生选修的每门课程存储成绩。

要求:每门课程仅设一次授课,且数据库仅包含一个学期数据。

可以明显看到,实体为:教授 Professor (PID, PName, OfficeID), 学生 Student (SID, SName), 课程 Course (CID, CName)

  • 因为每门课程都需要一位教授授课,教授和课程之间存在联系 taught by,并且 Professor:Course = N:1,且 Course 完全参与 taught by
  • 学生需要至少选修一门课程,学生和课程之间存在联系 takes,并且 Student:Course = 1:N,且 Student 完全参与 takes。因为每门课程仅设一次授课,因此 Course 完全参与 takes
  • 成绩是对应每位学生和课程的,因此可以将成绩 Grade 作为联系 takes 的属性。

所以 ER 图如下:

这一题还是比较简单的,基本是最基础的 ER 图转换。下面是一个稍微复杂点的例子:

  1. 我们需要设计一个关系数据库,用于存储某大型企业内部培训项目的信息。根据以下描述设计实体关系图:

  • 数据库存储公司每位员工的信息。每位员工拥有姓名和唯一的员工编号。
  • 培训项目中的每门课程都有唯一的课程编号和名称。
  • 课程由公司员工授课并参与学习。
  • 同一课程可多次开设,每次开设具有唯一的开设编号(在该课程内唯一)、具体日期和时间。
  • 每次开设仅由一名员工授课。
  • 数据库存储参与课程学习的员工成绩。

我们同样可以看出实体:Employee (EmpNo, Name), Course (CourseNo, Name)。考虑到开设课程需要记录开设编号、具体日期和时间,因此可以创建一个实体 Offering。考虑到它的属性,包含开设编号、具体日期和时间。但是因为题目说明开设需要和课程相关,所以可以判定开设会被 CourseNoOfferingNo 唯一确定一条记录,所以 Offering 是一个弱实体集。

因为每次开设都由一名员工授课,说明 Offering 完全参与 Teaches,并且 Offering:Employee = N:1。此时我们在设计 Offering 表时,需要加入一个外码 EmpNo 作为外码约束。

员工需要学习课程,但没有说明参与的约束情况,所以存在一个联系集 EnrolledEmployee:Course = M:N。员工成绩需要记录,并且与学习的课程有关,所以成绩 Grade 属性被加入 Enrolled 中。

综合来看,ER 图如下:

至于为什么在 Offering 表中存在一个外码 EmpNo,因为这遵循了关系模型的外码约束,在下一章复习会说明。


关系模型#

关系 (Relation) 是用于描述数据的主要结构,关系由 关系模式 (Relation Schema) 和 关系实例 (Relation Instance) 组成。关系模式描述了关系中的属性,关系实例描述了关系中的数据,一般以表格形式展示。例如:Student (SID, SName, SDept, SGPA) 是一个关系模式,Student (1, John, CS, 3.5) 是一个关系实例。

对于关系实例,它可以看成是一行行 元组 (Tuple) 的集合,一个元组是一个 记录 (Record),记录由 属性值 (Attribute Value) 组成。

对于关系模型而言,其最重要的是它的 完整性约束 (Integrity Constraint),用于保证关系实例的完整性。这些约束源于数据库所表示的微观世界中的规则。。

下面是几种最常见的关系模型完整性约束:

  1. 域约束 (Domain Constraint): 属性值受到其值域的限制,属性值必须在给定的值域内。例如:定义 SID 属性,它的值域为 [1, 100],那么Student (1, John, CS, 3.5) 是一个有效的关系实例,而 Student (101, John, CS, 3.5) 是一个无效的关系实例。
  2. 主码约束 (Primary Key Constraint): 一个关系实例必须具有 唯一非空 的主码,主码是关系实例的标识符。例如:定义 Student (SID, SName, SDept, SGPA),其中 SID 是主码,SID 对于所有的记录而言必须是唯一的,不能为空。
  3. 外码约束 (Foreign Key Constraint): 外码用于维护表与表之间的关系。它确保在一个表中的值必须在另一个表中存在,从而保持数据的一致性。外码通常用于实现一对多(One-to-Many)或多对多(Many-to-Many)的关系。之后会有详细的例子说明。
  4. 参照完整性约束 (Referential Integrity Constraint): 引用完整性约束用于确保数据在表中的引用关系是正确的。例如,如果一个表中的某个字段引用了另一个表中的主码字段,那么该字段的值必须在另一个表中存在,并且取值一致。

根据这些约束规则,我们可以将 ER 图转换为关系模型,下面是转换的规则:

  • 转换实体集:每个实体集对应一个关系表,关系表的模式就是实体集的属性名称。
    • 强实体集转换:先创建一个包含所有属性的关系模式,再标记主码。
    • 弱实体集转换:先创建一个包含所有属性的关系模式,再 添加依赖的实体集的主码作为外码,与弱实体集的部分码合并做主码。例如:Project 有两个属性 proj_iddateproj_id 是部分码。Project 依赖于 Department,于是添加 Department 的主码 dept_id 为外码,联合部分码 proj_id 一起为主码,即Project 的主码为 {proj_id, dept_id}
  • 转换联系集:
    • 1:1 关系: 将 非完全参与 的实体集的主码作为外码 插入到完全参与的实体集中。例如:一个部门只能且一定有一位员工管理,一位员工只能最多管理一个部门。Employee 的主码为 emp_idDepartment 的主码为 dept_id。此时,Department 是完全参与,所以将emp_id作为外码插入到Department中。Department的属性为 dept_id, emp_id, ...,主码还是 dept_id
      当然,如果双方实体都是部分参与,则可以采用 在任意一方加入另一方的主码作为外码,或者为它们的联系集 设计一个独立表,包含双方的主码作为外码,主码任意其中一个即可。
    • 1:N 关系: 在 N 的一侧的实体集中加入另一个实体集的主码作为外码。例如:一个员工只能属于一个部门,一个部门可以有多个员工。此时,外码约束的存在确保了部门 Department 中的所有员工都存在 Employee,即每一位员工都对应一个部门,此时部门的主码 dept_id 作为外码插入到 Employee 中。注意:反过来将 emp_id 作为外码存在 Department 表里是错误的,如果在 Department 放外码,一个部门就只能关联一个员工,违背了1:N的语义。
    • M:N 关系: 在多对多的关系中,通常的做法是 创建独立的关系表,包含双方主码作为外码,主码是外码的组合。例如:员工可以选择任意课程,课程可以包含任意员工。这种情况下需要创建中间表 Takes,包含emp_idcourse_id两个外码,并且它们联合为主码,即 Takes 的主码为 {emp_id, course_id}

有了这些规则之后,我们就可以将 ER 图转换为关系模型了。


关系模型例题#

下面是一些转换 ER 图的例子:

  1. 请将下面的 ER 图转换成关系模型。

根据转换规则,我们先转换实体集:

强实体集有:BUS, ROUTE, DRIVER.

  • BUS (licence, capacity)
  • ROUTE (number, departure station, destination station)
  • DRIVER (id, name, phone)

SCHEDULE 是弱实体集,它完全参与 ROUTE 的关系,所以将 ROUTE 的主码 number 作为外码插入到 SCHEDULE 中,并且与部分码 departure time 合并做主码,于是有:

  • SCHEDULE (number, departure time)

接下来我们来转换联系集。我们发现 BUSSCHEDULE 之间存在 M:N 的关系,DRIVERSCHEDULE 也存在 M:N 的关系。所以这两个联系集都需要创建中间表:

  • DRIVES (id, number, departure-time)
  • BUS-IN-USE (license, number, departure-time)

下面是一题稍微复杂的实际性问题:

  1. 请将下面的 ER 图转换成关系模型。

同样的,根据转换规则,我们先转换实体集:

强实体集有:branch, customer, loan, employee, account (忽略了多值属性)

  • branch (branch-name, assets, branch-city)
  • customer (customer-id, customer-name, customer-city, customer-street)
  • loan (loan-number, amount)
  • employee (employee-id, employee-name, dependent-name, telephone-number, employment-length, start-date)
  • account (account-number, balance)

弱实体集有: payment,它完全参与 loan 的关系 loan-payment,所以将 loan 的主码作为外码添加到 payment 中,并与部分码 payment-number 形成主码。

  • payment (payment-number, loan-number, payment-amount, payment-date)

接下来转换联系集:

对于 1:N 的关系:

  • 对于自反关系 works-for,将 1 侧的主码作为外码添加到 N 侧。此时两侧是一样的,所以会进行一些重命名。修改后的 employeeemployee (employee-id, employee-name, dependent-name, telephone-number, employment-length, start-date, manager-id)
  • 关系 cust-banker 重复同样的操作,可以得到:customer (customer-id, customer-name, customer-city, customer-street, employee-id, type) 注意联系集的属性也需要添加。
  • 关系 loan-branch 重复同样的操作,可以得到:loan (loan-number, amount, branch-id)

对于 M:N 的关系:

  • 关系 depositor 提取出两侧的主码作为外码,并合并做主码,可以得到:deposit (customer-id, account-number, access-date)
  • 关系 borrower 提取出两侧的主码作为外码,并合并做主码,可以得到:borrower (loan-number, customer-id)

所以最后我们得到了如下的关系模型:

  • branch (branch-name, assets, branch-city)
  • customer (customer-id, customer-name, customer-city, customer-street, employee-id, type)
  • loan (loan-number, amount, branch-id)
  • employee (employee-id, employee-name, dependent-name, telephone-number, employment-length, start-date, manager-id)
  • account (account-number, balance)
  • payment (payment-number, loan-number, payment-amount, payment-date)
  • deposit (customer-id, account-number, access-date)
  • borrower (loan-number, customer-id)

SQL#

SQL (Structured Query Language) 是一种用于管理关系数据库的语言。SQL由数据操作语言 DML (Data Manipulation Language)、数据定义语言 DDL (Data Definition Language)、数据控制语言 DCL (Data Control Language) 组成。

下面是一些 SQL 的基本语法:

增删改和约束#

  • 创建表:
CREATE TABLE [IF NOT EXISTS] <table_name> (
<field_name> <data_structure>,
...
);
  • 删除表:
DROP TABLE [IF EXISTS] <table_name>;
  • 插入:
-- Insert with all values
INSERT INTO <table_name>
VALUES (field1, field2, ...);
-- Insert with partial values
INSERT INTO <table_name> (col1, col2, ...)
VALUES (field1, field2, ...);
  • 修改数据:
UPDATE <table_name>
SET field1 = value1, field2 = value2,...
WHERE <condition>;
  • 删除数据:
-- Delete with condition
DELETE FROM <table_name> WHERE <condition>;
-- Delete all records
DELETE FROM <table_name>;
  • 约束
-- Primary KEY
CREATE TABLE [IF NOT EXISTS] <table_name> (
field_name data_structure PRIMARY KEY,
...
);
-- Foreign KEY
CREATE TABLE [IF NOT EXISTS] <table_name> (
PersonID int,
FOREIGN KEY (PersonID) REFERENCES Persons(PersonID),
...
);
-- NOT NULL
CREATE TABLE [IF NOT EXISTS] <table_name> (
field_name data_structure PRIMARY KEY NOT NULL,
...
);
-- Unique
CREATE TABLE [IF NOT EXISTS] <table_name> (
field_name data_structure UNIQUE,
...
);
-- Default Value
CREATE TABLE [IF NOT EXISTS] <table_name> (
field_name data_structure DEFAULT XXX,
...
);

查询语句及其语法#

查询语句的使用频率最高,下面详细介绍了查询语句中的部分:

SELECT 的语句顺序为:

SELECT [DISTINCT] <attributes_list> -- DISTINCT: 去重
FROM <table_list> -- FROM: 表的集合,可以多个表,可以使用 AS 重命名
[WHERE <condition>] -- WHERE: 条件
[GROUP BY <group_attributes_list>] -- GROUP BY: 将group_attributes 相同的记录分组放在一起
[HAVING <group_condition>] -- HAVING: 对GROUP BY 之后的分组进行条件筛选
[ORDER BY <order_attributes_list>] -- ORDER BY: 排序 SELECT 之后的结果表格

重命名、字符匹配和排序#

重命名:可以使用 ASattributes_listtable_list 进行重命名。在语句中 AS 可以省略。

SELECT <attributes_list> [AS] <attributes_alias>
FROM <table_list> [AS] <table_alias>
WHERE <condition>;

字符匹配:可以通过 LIKE 语句匹配字符,进行模糊查询。可以通过 % 匹配任意字符,通过 _ 匹配任意一个字符。

SELECT <attributes_list>
FROM <table_list>
WHERE <condition> LIKE <pattern>;

例如:找到所有学生中名字包含 “JONES” 的学生。

SELECT *
FROM student
WHERE name LIKE '%JONES%';

排序:可以使用 ORDER BY 对查询后的 结果 进行排序。

  • 升序 (默认):ASC
  • 降序:DESC
SELECT <attributes_list>
FROM <table_list>
ORDER BY <order_attributes_list>;

集合运算和空检测#

集合运算:在 SQL 中可以使用集合运算符对关系进行运算,例如 交 \cup INTERSECT, 并 \cap UNION , 减 - EXCEPT

下面是几个集合运算的例子:

对于如下关系模式,* 表示主码:

Sailor (sid*, sname, rating, age);
Boat (bid*, bname, color);
Reservation (sid*, bid*, day*);
  1. 找到被预留的红船 绿船的 bid
SELECT B.bid
FROM reservation AS R, boat AS B
WHERE B.bid = R.bid AND (B.color = 'red' OR B.color = 'green');
-- 等价于两个查询的结果的并集
SELECT B.bid
FROM reservation AS R, boat AS B
WHERE B.bid = R.bid AND B.color = 'red';
UNION
SELECT B.bid
FROM reservation AS R, boat AS B
WHERE B.bid = R.bid AND B.color = 'green';
  1. 找到预留所有船的船员名字。那么可以翻译为,不存在没有被预订的船,我们可以先查询所有的船,再使用 EXCEPT 减去所有有预订的船就行。
SELECT S.sname
FROM sailor AS S
WHERE NOT EXISTS ( -- 找到所有的船
SELECT B.bid
FROM Boat AS B
) EXCEPT ( -- 减去所有有预订的船
SELECT R.bid
FROM reservation AS R
WHERE R.sid = B.sid
)

空 / 数量检测:可以通过 EXISTS, NOT EXISTS, IS NULLNOT NULL 检测一个关系是否为空。还可以通过 ANY, SOME, ALL 语句检测一个关系是否满足某个条件。

聚合函数#

聚合函数:可以通过 COUNT, SUM, AVG, MAX, MIN 函数对关系进行聚合,并且计算一个结果。

  • COUNT ([DISTINCT] <attributes_list>): 计算关系中 attributes_list 的数量。
  • SUM ([DISTINCT] <attributes_list>): 计算关系中 attributes_list 的和。
  • AVG ([DISTINCT] <attributes_list>): 计算关系中 attributes_list 的平均值。AVG 会自动忽略NULL值。
  • MAX (<attributes_list>): 计算关系中 attributes_list 的最大值。
  • MIN (<attributes_list>): 计算关系中 attributes_list 的最小值。

例如:找到最老的船员的名字与年龄。

SELECT S.sname, S.age
FROM sailor AS S
WHERE S.age = (
SELECT MAX(S1.age)
FROM sailor AS S1
)

这里使用了嵌套查询,因为除了使用分组外,SELECT 的字句中若要使用聚合函数,则 不能存在任何非聚合函数的属性列

例如一个错误示例:

SELECT S.Sname, MAX(S.age) -- ❌聚合函数不能与非聚合函数的属性列一起使用
FROM sailor AS S

分组查询#

分组:通过 GROUP BY 对关系进行分组,可以找到同样属性值的记录。HAVING 用于对分组后的结果进行筛选。

我们通过一些例子来理解:

  1. 找出每个等级中 年龄至少为18岁 的最年轻水手,每个等级至少需有两名符合 此条件 的水手。(Find the age of the youngest sailor with age > 18, for each rating with at least 2 such sailors)
SELECT S.rating, MIN(S.age) AS min_age -- 仅在GROUP BY存在时才允许聚合函数与其他属性列一起使用
FROM sailor AS S
WHERE S.age > 18 -- 先找到所有大于18岁的
GROUP BY S.rating -- 通过等级进行分组
HAVING COUNT(S.sid) >= 2 -- 再筛选出等级至少两位的
  1. 找到每一个红船被预订的数量。可以转换为:找到所有被预订的红船,再通过 GROUP BY 进行分组计算其数量。
SELECT B.bid, COUNT(R.sid) AS RESERVATION_COUNT
FROM reservation AS R, boat AS B
WHERE B.bid = R.bid AND B.color = 'red' -- 找到所有被预订的红船
GROUP BY B.bid -- 通过bid进行分组

注意,这里不能先找到所有预订的船,再在 HAVING 处再筛选红船,因为只有在 GROUP BY 后形成的属性列的属性才能进行 HAVING 筛选。

SELECT B.bid, COUNT(R.sid) AS RESERVATION_COUNT
FROM reservation AS R, boat AS B
WHERE B.bid = R.bid
GROUP BY B.bid
HAVING B.color = 'red' -- ❌ B.color不是分组之后形成的属性列,不能进行HAVING筛选
  1. 找到所有等级船员中平均年龄最小的等级。可以转换为:将船员按照等级进行分组,计算每个分组的平均年龄,再找到最小的等级。
SELECT T.rating, T.avg_age
FROM ( -- 从分组好的表中进一步查找条件
SELECT S.rating, AVG(S.age) AS avg_age
FROM sailor AS S
GROUP BY S.rating
) AS T
WHERE T.avg_age = ( -- 找到年龄最小的
SELECT MIN(T1.avg_age)
FROM T
);

注意:聚合函数不能自身嵌套,下面是错误示例:

SELECT S.rating
FROM sailor AS S
WHERE S.age = (
SELECT MIN(AVG(S1.age)) -- ❌聚合函数不能自身嵌套
FROM sailor AS S1
GROUP BY S1.rating
)

连接#

连接 (Join):连接可以查询两个表中的相同字段的记录。连接分为:内连接 (Inner Join),外连接 (Outer Join),自然连接 (Natural Join)。

  • 内连接 (Inner Join):用于查询两个表之间相同字段的记录,只返回两表中”有匹配”的记录。一般通过 INNER JOIN ... ON 语句启用,或 WHERE 中进行连接。INNER JOIN ... ON 可以省略。
  • 外连接 (Outer Join):用于查询两个表之间相同字段的记录,并且包含其他信息。它又可以分为左外连接 (Left Outer Join),右外连接 (Right Outer Join),全外连接 (Full Outer Join)。
  • 自然连接 (Natural Join):自动识别同名列进行等值连接,且结果中同名列只出现一次。它与内连接 (Inner Join) 类似,但是它不包含 ON 子句。

下面是三种连接的例子:

若有如下表格:

Student:

SID | SName | DeptID
-----+--------+--------
001 | Alice | 10
002 | Bob | 20
003 | Carol | 30
004 | David | NULL

Departments:

DeptID | DeptName
-------+------------------
10 | Computer Science
20 | Mathematics
40 | Physics

我们执行内连接:

SELECT Students.SID, Students.SName, Departments.DeptName
FROM Students
INNER JOIN Departments ON Students.DeptID = Departments.DeptID;
-- 隐式写法
SELECT Students.SID, Students.SName, Departments.DeptName
FROM Students, Departments
WHERE Students.DeptID = Departments.DeptID;

执行的结果是:

SID | SName | DeptName
-----+--------+------------------
001 | Alice | Computer Science
002 | Bob | Mathematics

内连接只包含匹配的记录。而外连接分为左外连接 (Left Outer Join),右外连接 (Right Outer Join),全外连接 (Full Outer Join)。其区别就是在匹配的记录的基础上,再添加左表/右表/两表非匹配的记录。

SELECT Students.SID, Students.SName, Departments.DeptName
FROM Students
LEFT JOIN Departments ON Students.DeptID = Departments.DeptID;

这是一个左外连接,它 保留左表所有记录,右表无匹配时填 NULL,结果为:

SID | SName | DeptName
-----+--------+------------------
001 | Alice | Computer Science
002 | Bob | Mathematics
003 | Carol | NULL ← 保留了Carol
004 | David | NULL ← 保留了David

同理,右外连接保留右表所有记录,左表无匹配时填 NULL

SELECT Students.SID, Students.SName, Departments.DeptName
FROM Students
RIGHT JOIN Departments ON Students.DeptID = Departments.DeptID;

结果为:

SID | SName | DeptName
-----+--------+------------------
001 | Alice | Computer Science
002 | Bob | Mathematics
NULL | NULL | Physics ← 保留了Physics

全外连接 (FULL OUTER JOIN) 保留两表所有记录,无匹配的都用 NULL 填充。两张表的所有记录都出现,它是 LEFT JOIN + RIGHT JOIN 的并集。

SELECT Students.SID, Students.SName, Departments.DeptName
FROM Students
FULL OUTER JOIN Departments ON Students.DeptID = Departments.DeptID;

结果为:

SID | SName | DeptName
-----+--------+------------------
001 | Alice | Computer Science
002 | Bob | Mathematics
003 | Carol | NULL ← 左表未匹配
004 | David | NULL ← 左表未匹配
NULL | NULL | Physics ← 右表未匹配

对于自然连接 (Natural Join),它与内连接 (Inner Join) 类似,它自动 识别同名列进行等值连接,且结果中同名列只出现一次 (正常内连接中同名列会重复)。

SELECT *
FROM Students
NATURAL JOIN Departments;

结果是:

DeptID | SID | SName | DeptName
-------+------+--------+------------------
10 | 001 | Alice | Computer Science
20 | 002 | Bob | Mathematics

下面是三种连接对比总结表:

连接类型返回记录匹配条件未匹配处理使用场景
INNER JOIN两表都匹配的必须显式指定丢弃只需要完全匹配数据
LEFT JOIN左表全部 + 匹配的右表必须显式指定右表填NULL主表数据必须全部显示
RIGHT JOIN右表全部 + 匹配的左表必须显式指定左表填NULL(较少用,可用LEFT替代)
FULL JOIN两表全部记录必须显式指定双方填NULL需要完整数据集合
NATURAL JOIN两表匹配的(内连接)自动识别同名列丢弃表设计规范且字段明确
  • 视图 (View): 视图是从其他表格中派生出的虚拟表,只允许有限的操作,以方便表示某些操作,保护数据安全。

视图创建语法,相当于在查询语句外部添加了一个新表格创建:

CREATE VIEW view_name AS
SELECT <attributes_list>
FROM <table_name>
WHERE <condition>;

若视图不再需要,则可以直接删除它:

DROP VIEW view_name;

如果需要更新视图中的数据,并使其修改能映射到基本的表格中,则需要使用 UPDATE 语句:

UPDATE view_name
SET <attribute_name> = <new_value>
WHERE <condition>;

SQL 经典例题#

以上是 SQL 的基础语法,下面我们根据一些例题来加深理解。

逻辑问题#

对于如下关系模式,* 表示主码:

Sailor (sid*, sname, rating, age);
Boat (bid*, bname, color);
Reservation (sid*, bid*, date*);
  1. Find sid’s of sailors who’ve reserved a red or a green boat
    找到所有预订了红色 绿色船的船员的sid

对于题目1,其中的关键词 or 表示:只要预订过至少一艘红色或绿色船即可,bid 需要出现在 Reservation 表中,并且bid 对应的 color 需要是红色或绿色。

我们可以使用内连接,它只保留了匹配的记录:

SELECT DISTINCT R.sid
FROM Reservation R
JOIN Boat B ON R.bid = B.bid
WHERE B.color = 'red' OR B.color = 'green';
-- 或隐式写法
SELECT DISTINCT R.sid
FROM Reservation R, Boat B
WHERE R.bid = B.bid AND (B.color = 'red' OR B.color = 'green');

当前,我们还可以拆分为两个查询:先找到预订了这些船的所有船员,再找出红色或绿色的船。

SELECT DISTINCT R.sid
FROM Reservation R
WHERE R.bid IN (
SELECT B.bid
FROM Boat B
WHERE B.color = 'red' OR B.color = 'green'
);

此外,还可以采用集合的思想,将两个查询的结果集进行并集:

SELECT DISTINCT R.sid
FROM Reservation R, Boat B
WHERE R.bid = B.bid AND B.color = 'red'
UNION
SELECT DISTINCT R.sid
FROM Reservation R, Boat B
WHERE R.bid = B.bid AND B.color = 'green';
  1. Find sid’s of sailors who’ve reserved a red and a green boat
    找到所有预订了红色 绿色船的船员的sid

对于题目2,它与题目1的要求相似,只是要求预订了红色 绿色船。但是我们 不能 直接将 OR 改为 AND,因为 一艘船不可能同时是红色和绿色WHERE 的条件是对 同一行记录的约束

-- ❌ 错误!这会返回空集
SELECT DISTINCT R.sid
FROM Reservation R
JOIN Boat B ON R.bid = B.bid
WHERE B.color = 'red' AND B.color = 'green';

正确的解法是使用自连接 (Self-Join),即拓展一下连接表格,添加 color 为重复的列,分别匹配 redgreen

SELECT DISTINCT R1.sid
FROM Reservation R1
JOIN Boat B1 ON R1.bid = B1.bid
JOIN Reservation R2 ON R1.sid = R2.sid
JOIN Boat B2 ON R2.bid = B2.bid
WHERE B1.color = 'red' AND B2.color = 'green';

R1找红色船的预订,R2找绿色船的预订,通过 R1.sid = R2.sid 找到同时满足两个条件的船员

当然这么写太复杂了,一般情况下我们会使用 两次 EXISTS 来解决:

SELECT DISTINCT S.sid
FROM Sailor S
WHERE EXISTS ( -- 找到预订了红色船的船员
SELECT *
FROM Reservation R1, Boat B1
WHERE R1.sid = S.sid
AND R1.bid = B1.bid
AND B1.color = 'red'
)
AND
EXISTS ( -- 找到预订了绿色船的船员
SELECT *
FROM Reservation R2, Boat B2
WHERE R2.sid = S.sid
AND R2.bid = B2.bid
AND B2.color = 'green'
);

这是最清楚的解法,分步完成。当然,我们也可以将其改为交集的形式:

SELECT DISTINCT R.sid
FROM Reservation R, Boat B
WHERE R.bid = B.bid AND B.color = 'red'
INTERSECT
SELECT DISTINCT R.sid
FROM Reservation R, Boat B
WHERE R.bid = B.bid AND B.color = 'green';
  1. Find sid’s of sailors who’ve reserved a red boat BUT NOT a green boat
    找到预订了红色船 但没有 预订绿色船的船员

题目3意味着:必须预订至少一艘红色船,但从未预订绿色船。我需要找到的是集合:{红色船船员} - {绿色船船员}

我们可以使用 NOT EXISTSNOT IN,用于排除绿船船员:

SELECT DISTINCT R.sid
FROM Reservation R
JOIN Boat B ON R.bid = B.bid
WHERE B.color = 'red' -- 外层找预订了红色船的船员
AND NOT EXISTS ( -- NOT EXISTS 排除那些预订了绿色船的船员
SELECT *
FROM Reservation R2
JOIN Boat B2 ON R2.bid = B2.bid
WHERE R2.sid = R.sid -- 注意:嵌套的地方需要满足上层查询的条件
AND B2.color = 'green'
);

当前,运用集合减的知识也可以清楚解决:

SELECT DISTINCT R.sid
FROM Reservation R, Boat B
WHERE R.bid = B.bid AND B.color = 'red'
EXCEPT
SELECT DISTINCT R.sid
FROM Reservation R, Boat B
WHERE R.bid = B.bid AND B.color = 'green';
  1. Find sid’s of sailors who’ve reserved EVERY red boat.
    找到预订了 所有 红色船的船员

题目4要求找到一个船员,他预订了所有的红色船。这意味着 不存在一艘红色船是该船员没有预订的

所以根据这个逻辑,运用 NOT EXISTS 或 集合减 EXCEPT 可以得到:

SELECT S.sid
FROM Sailor S
WHERE NOT EXISTS ( -- 所有红色船的记录
SELECT B.bid
FROM Boat B
WHERE B.color = 'red'
AND NOT EXISTS ( -- 船员预订的记录
SELECT *
FROM Reservation R
WHERE R.sid = S.sid
AND R.bid = B.bid
)
);
  1. Find sid’s of sailors who’ve reserved ONLY red boats
    找到 预订了红色船的船员(不能预订其他颜色)

翻译一下题目5就是:一个船员至少预订了一艘船,且不存在颜色不是红色的船。

充分运用集合的方法:

SELECT S.sid
FROM Sailor S
WHERE EXISTS ( -- 至少预订了一艘船
SELECT *
FROM Reservation R
WHERE R.sid = S.sid
)
AND NOT EXISTS ( -- 不存在预订非红色船的记录
SELECT *
FROM Reservation R
JOIN Boat B ON R.bid = B.bid
WHERE R.sid = S.sid
AND B.color <> 'red'
);
  1. Find sid’s of sailors whose rating is greater than SOME sailor named ‘Bob’
    找到rating大于 某个 名叫Bob的船员的所有船员

题目6是一个存在量词的题目,我们可以使用 ANYSOME 去解决这个逻辑问题,或者使用聚合函数。

SELECT S.sid
FROM Sailor S
WHERE S.rating > ANY (
SELECT S2.rating
FROM Sailor S2
WHERE S2.sname = 'Bob'
);
SELECT S.sid
FROM Sailor S
WHERE S.rating > ( -- 使用聚合函数
SELECT MIN(S2.rating)
FROM Sailor S2
WHERE S2.sname = 'Bob'
);
  1. Find sid’s of sailors whose rating is greater than ALL sailors named ‘Bob’
    找到rating大于 所有 名叫Bob的船员的船员

这题和题目6的差别是,名叫 “Bob” 的船员可能有很多个,我们可以使用 ALL 代替即可。

SELECT S.sid
FROM Sailor S
WHERE S.rating > ALL (
SELECT S2.rating
FROM Sailor S2
WHERE S2.sname = 'Bob'
);
SELECT S.sid
FROM Sailor S
WHERE S.rating > ( -- 使用聚合函数
SELECT MAX(S2.rating)
FROM Sailor S2
WHERE S2.sname = 'Bob'
);
  1. Find sid’s of sailors who’ve reserved a red boat OR a green boat BUT NOT a blue boat.
    找到预订了红色 绿色船 但没 预订蓝色船的船员

题目8是题目1和题目3的复合形式,我们可以组合他们得到结果:

SELECT DISTINCT R.sid
FROM Reservation R
JOIN Boat B ON R.bid = B.bid
WHERE (B.color = 'red' OR B.color = 'green')
AND NOT EXISTS (
SELECT *
FROM Reservation R2
JOIN Boat B2 ON R2.bid = B2.bid
WHERE R2.sid = R.sid
AND B2.color = 'blue'
);

当然,可以使用非常显而易见的集合运算代替:

SELECT DISTINCT R.sid
FROM Reservation R, Boat B
WHERE R.bid = B.bid AND B.color = 'red'
UNION
SELECT DISTINCT R.sid
FROM Reservation R, Boat B
WHERE R.bid = B.bid AND B.color = 'green'
EXCEPT
SELECT DISTINCT R.sid
FROM Reservation R, Boat B
WHERE R.bid = B.bid AND B.color = 'blue';
  1. Find sid’s of sailors who’ve reserved EXACTLY two boats
    找到 恰好 预订了两艘船的船员

分析题目9可以发现,它需要我们按照预订船的数量进行分组,然后筛选出预订数为2的船员,使用 GROUP BYHAVING 来解决。

SELECT R.sid
FROM Reservation R
GROUP BY R.sid
HAVING COUNT(DISTINCT R.bid) = 2;
  1. Find sid’s of sailors who have the same rating as some sailor named ‘Bob’
    找到与某个Bob有相同rating的船员

这一题我们需要先找到 Bob 的 rating,再排除他自己:

SELECT S.sid
FROM Sailor S
WHERE S.rating IN (
SELECT S2.rating
FROM Sailor S2
WHERE S2.sname = 'Bob'
)
AND S.sname <> 'Bob'; -- 排除Bob自己

逻辑问题总结#

总结一下这些逻辑问题,我们可以知道如下逻辑及其解决方案:

自然语言SQL实现策略推荐方法
OR(或)内连接 / 取并集直接 JOIN + OR
AND(且)自连接 / 多重 EXISTS / 取交集INTERSECT
BUT NOT(但不)多重 NOT EXISTS / 取减集NOT EXISTS / EXCEPT / NOT IN
EVERY(所有)转换为双重否定 NOT EXISTSNOT EXISTS / EXCEPT / NOT IN
SOME/ANY(某些)> ANY (subquery)ANY / SOME
ALL(全部)转换为双重否定 NOT EXISTSNOT EXISTS / EXCEPT / NOT IN
ONLY(仅)NOT EXISTS 非目标NOT EXISTS
EXACTLY N(恰好N个)HAVING COUNT = NGROUP BY + HAVING
AT LEAST N(至少N个)HAVING COUNT >= NGROUP BY + HAVING
AT MOST N(最多N个)HAVING COUNT <= NGROUP BY + HAVING
MORE THAN / LESS THAN(多于/少于)子查询比较(SELECT X) 比较 (SELECT B)

此外,在写嵌套查询时,我们时常需要匹配上级查询和下级查询的字段,但时常不知道什么时候匹配,什么时候不匹配。

答案是:当且仅当子查询需要”知道” 当前检查的是哪个属性值时,才需要用外层查询的属性值来匹配。

例如:Find the names of sailors who have reserved bid=103

SELECT S.sname
FROM Sailor S
WHERE EXISTS (
SELECT *
FROM Reservation R
WHERE R.bid = 103
AND R.sid = S.sid
);

如果没有 R.sid = S.sid,那么只要 存在 任何人预订了103号船,WHERE 条件就为真,所有船员都会被返回。

当有 R.sid = S.sid 时,针对每个船员单独判断,只返回真正预订了103号船的人。

但是如果我们将 EXISTS 改为 IN,那么子查询只执行一次,且不使用外层查询的值,结果集在外层查询前就确定了,这样的性能更高。且结果正确。

SELECT S.sname
FROM Sailor S
WHERE S.sid IN (
SELECT R.sid
FROM Reservation R
WHERE R.bid = 103
);

我们一般将 EXISTSNOT EXISTS 称为 相关子查询,而INNOT IN 称为 非相关子查询。

所以,总结来说什么时候需要显式关联:

查询类型是否需要显式关联原因
EXISTS 子查询必须子查询不返回具体值,只返回TRUE/FALSE
NOT EXISTS 子查询必须同上
IN 子查询不需要子查询返回值列表,外层通过IN隐式关联
NOT IN 子查询不需要同上(但要注意NULL陷阱)
比较运算符子查询不需要WHERE age > (SELECT AVG(age) ...)
JOIN不需要ON子句已经处理关联

其中,特别注意 NOT IN它可能会返回 NULL。如果表示不存在,一般使用 NOT EXISTS

分组问题#

  1. For each red boat, display the bid and the number of reservations for this boat.
    找到所有红色船的bid和预订数量

根据题目,可以解析出:需要遍历每一艘红色船,统计每艘船的预订数量

  • “For each red boat” → 按红色船分组(GROUP BY)
  • “display the bid” → 选择 bid 列
  • “number of reservations” → 统计预订次数(COUNT)
SELECT B.bid, COUNT(R.sid) AS reservation_count
FROM Boat B, Reservation R
WHERE B.bid = R.bid AND B.color = 'red'
GROUP BY B.bid;
  1. For each red boat, display the bname and the number of reservations for this boat.
    找到所有红色船的bname和预订数量

这题和题目11类似,只是需要 B.bname 列。 但是纯在一个陷阱:因为B.bname 不是主码,如果多艘船有相同的名字,GROUP BY 的行为会改变。

SELECT B.bid, B.bname, COUNT(R.sid) AS reservation_count
FROM Boat B, Reservation R
WHERE B.bid = R.bid AND B.color = 'red'
GROUP BY B.bid, B.bname;

这里用到了一个重要的SQL规则:SELECT 子句中的非聚合列必须出现在 GROUP BY 中

所以:

SELECT B.bid, B.bname, COUNT(R.sid)
FROM ...
GROUP BY B.bname; -- ❌ 错误!bid 没在 GROUP BY 中!

至于为什么一定需要分组中添加 bid,我们通过数据来演示:

假设数据:

Boat:
bid | bname | color
----+--------+------
101 | Sunset | red
103 | Sunset | red
105 | Storm | red

GROUP BY B.bname:

SELECT B.bname, COUNT(R.sid)
FROM Boat B
LEFT JOIN Reservation R ON B.bid = R.bid
WHERE B.color = 'red'
GROUP BY B.bname;

因为只按照 bname 分组,所以结果为:

bname | reservation_count
-------+------------------
Sunset | 3 ← 101和103的预订合并了!
Storm | 0

GROUP BY B.bid, B.bname(按船只和船名分组):

SELECT B.bid, B.bname, COUNT(R.sid)
FROM Boat B
LEFT JOIN Reservation R ON B.bid = R.bid
WHERE B.color = 'red'
GROUP BY B.bid, B.bname;

结果:

bid | bname | reservation_count
----+--------+------------------
101 | Sunset | 2
103 | Sunset | 1 ← 分开统计!
105 | Storm | 0

所以如果需要区分不同的船只,必须在 GROUP BY 中包含主键(bid)。

总的来说,SELECT 子句中的列只能是:

  1. 聚合函数(COUNT, SUM, AVG, MAX, MIN 等)
  2. GROUP BY 子句中的列
  3. 常量
  1. Find the age of the youngest sailor with age > 18, for each rating with at least 2 sailors (of any age)
    找出每个 rating 级别中,年龄大于18岁的最年轻水手的年龄。只考虑那些至少有2名水手 (所有年龄) 的 rating

这是一个比较复杂的题目,我们拆解它:

  • “for each rating” → 按 rating 分组
  • “with at least 2 sailors” → HAVING COUNT(*) >= 2
  • “(of any age)” → 统计所有船员,不限年龄
  • “the youngest sailor with age > 18” → MIN(age) WHERE age > 18
SELECT S.rating, MIN(S.age) AS youngest_age
FROM Sailor S
WHERE S.age > 18
AND S.rating IN (
SELECT S2.rating
FROM Sailor S2
GROUP BY S2.rating
HAVING COUNT(*) >= 2
)
GROUP BY S.rating;
-- 或可以在 HAVING 中嵌套子查询
SELECT S.rating, MIN(S.age) AS youngest_age
FROM Sailor S
WHERE S.age > 18
GROUP BY S.rating
HAVING 1 < (
SELECT COUNT(*)
FROM Sailor S2
WHERE S2.rating = S.rating
);

分析题目可以发现:

  • 等级条件:至少有2位船员(任何年龄)
  • 在满足条件的等级中,找年龄 > 18的最年轻船员

这里的陷阱有:

  • 条件”至少2名水手”是针对 所有年龄 的水手
  • 但查找最小年龄时只考虑年龄 > 18的水手
  • 可能存在某个rating有2名水手,但都不满足age > 18的情况

所以,我们的思路可以是:

  1. 识别符合条件的 rating:作为子查询,按 rating 分组,统计每个 rating 的水手总数,筛选出至少有2名水手的 rating。
  2. 在符合条件的 rating 中查找最小年龄,只考虑年龄>18的水手,在步骤1筛选出的rating范围内,按rating分组,找出每组的最小年龄。

这里不能直接使用:HAVING COUNT(*) >= 2 因为先执行的 WHERE 已经过滤掉了小于18岁的船员,不满足题目要求的对任意年龄而言 rating 至少为2,会出现错误答案。

  1. Find those ratings for which the average age is the minimum overall ratings
    找到年龄平均值最小的rating

这一题需要我们先按照rating进行分组,然后计算每组年龄平均值,最后取其中年龄的最小值。所以使用一个嵌套查询,外层查询计算每组年龄平均值,内层查询计算每组年龄的最小值。

SELECT T.rating, T.min_age -- 不能直接使用 AVG(S.age) AS min_age 因为没有 GROUP BY 在外层
FROM (
SELECT S.rating, AVG(S.age) AS avg_age
FROM Sailor S
GROUP BY S.rating
) AS T;
WHERE T.min_age = (
SELECT MIN(T.avg_age)
FROM T
);

分组问题总结#

总结一下适用于 GROUP BY 的情况:

SQL 执行顺序是:FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY

其中我们需要记住:

  • WHERE 在 GROUP BY 之前(过滤 原始数据
  • HAVING 在 GROUP BY 之后(过滤 分组结果
  • SELECT 在 GROUP BY 之后(只能选择 分组列聚合结果

对于下面的标记,我们一般需要使用GROUP BY

信号词含义示例
For each对于每个…For each rating, count sailors
Per每个…Display reservations per boat
By按照…分组Average age by rating
Total总计(通常需要分组)Total reservations for each boat
Count/Sum/Average of each统计每个…Sum of salaries for each department

此外,SQL 有一个重要的子句限制:SELECT 中的 非聚合列必须出现在 GROUP BY 中。

-- 正确示例
SELECT rating, -- 在 GROUP BY 中
COUNT(*), -- 聚合函数
AVG(age) -- 聚合函数
FROM Sailor
GROUP BY rating;
-- 错误示例
SELECT rating, -- 在 GROUP BY 中
sname, -- ❌ 不在 GROUP BY 中!
COUNT(*)
FROM Sailor
GROUP BY rating;

我们可以从数据看到这个规则的原因:若一个 rating 中有多个船员,sname 不知道应该显示哪个。


关系代数与运算#

SQL 是结构化的关系模型操作语言,而形式化关系模型的基本操作集就是关系代数 (relational algebra)。这些操作使用户能够将基本的检索请求指定为关系代数表达式。检索查询的结果是一个新关系 (表)。

我们按照操作关系的数量和内容为关系代数进行分类,有:

  1. 一元关系 (unary operation): 选择 (select) σ\sigma投影 (project) π\pi重命名 (rename) ρ\rho
  2. 集合关系 (set operation): 集合交 (Intersection) \cap集合并 (Union) \cup集合差 (Difference) -笛卡尔积 (Cartesian product) / 叉乘 (Cross product) ×\times
  3. 二元关系 (binary operation): 连接 (join) condition\bowtie_{\text{condition}}等值连接 (equi-join) R.a=S.a\bowtie_{R.a=S.a}自然连接 (natural join) \bowtie除运算 (Division) ÷\div

一元关系#

  1. 选择 (selection) σcondition(R)\sigma_{condition} (R): 用于选择一个满足条件的元组。σ\sigma 可以嵌套即 σcondition1(σcondition2(R))=σcondition1condition2(R)\sigma_{condition_1}( \sigma_{condition_2}(R) ) = \sigma_{condition_1 \land condition_2}(R)
  2. 投影 (projection) πattributes(R)\pi_{attributes}(R): 用于选择一列属性。π\pi 会进行去重。
  3. 重命名 (rename) ρ(R,E)\rho (R, E): 用于给关系 EE 重命名 为 RR。还可以为关系的属性进行重命名:ρ(R(A1B1,A2B2,),E)\rho (R(A_1 \rightarrow B_1, A_2 \rightarrow B_2, \dots), E)RR 表格中的属性 AiA_i 重命名为 BiB_i

集合关系#

对于每个集合关系,要求两个关系 表格属性数相同,且对应属性的值域相同

  1. 并集 (union) RSR \cup S,表示 RRSS 的并集。
  2. 交集 (intersection) RSR \cap S,表示 RRSS 的交集。
  3. 差集 (difference) RSR - S,表示 RRSS 的差集。RS=RSRSR - S = R \cup S - R \cap S.
  4. 叉乘 (cross product) R×SR \times S,表示 RRSS 的叉乘,包含 RRSS 的所有属性和值,形成的新关系表格的属性顺序从左 (RR) 到右 (SS)。

例如:

R:
| a | b |
----+----
| C | 2 |
| D | 3 |
S:
| a | c | d |
----+---+----
| A | 5 | + |
| B | 7 | x |

叉乘结果为:

R × S:
| a_R | b | a_S | c | d |
------+---+-----+---+----
| C | 2 | A | 5 | + |
| C | 2 | B | 7 | x |
| D | 3 | A | 5 | + |
| D | 3 | B | 7 | x |

二元关系#

  1. 连接 (join) RconditionSR \bowtie_{\text{condition}} S,表示 RRSS 的连接,包含 RRSS 的所有属性和值,形成新的关系表格。RconditionS=σcondition(R×S)R \bowtie_{\text{condition}} S = \sigma_{\text{condition}} (R \times S)
  2. 等值连接 (equal join) RR.a=S.aSR \bowtie_{R.a = S.a} S,表示 RRSS 根据对应字段相等的连接,不包含对应属性的列,包含 RRSS 的其他列,形成新的关系表格。
  3. 自然连接 (natural join) RSR \bowtie S,表示 RRSS 根据所有同名字段相等的连接,不包含所有同名属性的列,包含 RRSS 的其他列,形成新的关系表格。

例如:

R:
| id | name | age |
-----+--------+------
| 1 | Alice | 18 |
| 2 | Bob | 19 |
| 3 | Charlie| 20 |
S:
| id | class |
-----+--------
| 1 | 1 |
| 2 | 1 |
| 5 | 4 |

对于等值连接 RR.id=S.idSR \bowtie_{R.id = S.id} S,结果为:

| id | name | age | class |
-----+--------+------+-------
| 1 | Alice | 18 | 1 |
| 2 | Bob | 19 | 1 |

该结果同样为自然连接 RSR \bowtie S,因为找到了所有同名字段 id 相等的元组。

  1. 除法 (Devision) R(X,Y)÷S(Y)=T(X)R(X,Y) \div S(Y) = T(X) 找出 RR 中那些与 SS 中所有 Y 值都有关联的 X 值。TT 中的每个元素,都必须和 SS 中的每一个元素配对出现在 RR 中。

例如下面的学生选课问题 RRSS 的例子,T=R÷ST = R \div S 的结果为:

R:
学生 | 课程
-------|------
张三 | 数学
张三 | 英语
张三 | 物理
李四 | 数学
李四 | 英语
王五 | 数学
S:
课程
------
数学
英语
R / S:
| 学生 | 数学 | 英语 | 结果 |
|------|------|------|------|
| 张三 | ✅ | ✅ | **保留** |
| 李四 | ✅ | ✅ | **保留** |
| 王五 | ✅ | ❌ | 舍弃 |
T:
学生
------
张三
李四

除法相当于是 SS 的条件下去筛选,并且去除掉没有匹配的行以及筛选的列。它不是简单的筛选,而是跨行验证完整性。

除法适用于处理 “全部”、“所有”、“每一个”相关的查询需求。它在 SQL 中经常会被翻译为逆否命题来解决。

关系代数例题和总结#

关系代数例题#

下表是关系代数的用途及其表达式的总结图:

常见的例题中,我们会将关系代数 与 SQL 语句的互相转换。

Convert the following algebra expressions to SQL (for simplicity, you can omit DISTINCT):

将下列代数表达式转换为 SQL(为简化起见,可省略 DISTINCT):

Employee (eid*, Ename, Salary),
Department (did*, Dname, eid),
Works(did*, eid*)
  1. πEname(σeid=5(Employee))\pi_{\text{Ename}}(\sigma_{\text{eid}=5}(\text{Employee}))

我们先从内层开始看,内层是一个选择语句,选择 Employee 表中 eid=5 的行。外层是个投影,只选取Ename列。

SELECT Ename
FROM Employee E
WHERE E.eid = 5;
  1. πeid(Employee)πeid(Works)\pi_{\text{eid}}(\text{Employee}) - \pi_{\text{eid}}(\text{Works})

这是一个减法,即先选取 Employee 表中的所有行,再从结果中减去 Works 表中的所有行。

SELECT E.eid
FROM Employee E
EXCEPT
SELECT W.eid
FROM Works W;

当然,我们也可以理解为找出 没有工作分配 的员工ID:

SELECT E.eid
FROM Employee E
WHERE NOT EXISTS (
SELECT *
FROM Works W
WHERE W.eid = E.eid
);
  1. πE.Ename(ρ(E,Employee)(πeid(Employee)πeid(Works)))\pi_{\text{E.Ename}} (\rho (E, \text{Employee}) \bowtie (\pi_{\text{eid}}(\text{Employee}) - \pi_{\text{eid}}(\text{Works})))

这一题是投影了Ename列,并使用NOT EXISTS子查询找出没有工作分配的。

SELECT E.Ename
FROM Employee E
WHERE NOT EXISTS (
SELECT *
FROM Works W
WHERE W.eid = E.eid
);
  1. πEmployee.Ename, Department.eid(EmployeeWorks(did), (did)Department)\pi_{\text{Employee.Ename, Department.eid}} (\text{Employee} \bowtie \text{Works} \bowtie_{\text{(did), (did)}} \text{Department})

这是一个连续的自然连接和等值连接,可以很轻松得到:

SELECT Employee.Ename, Department.eid
FROM Employee E, Works W, Department D
WHERE Employee.eid = Works.eid AND Works.did = Department.did;
  1. ρ(E1,Employee),ρ(E2,Employee),ρ(E,Employee)\rho (E1, \text{Employee}), \rho (E2, \text{Employee}), \rho (E, \text{Employee})
    πE.Ename(E((πE1.eidE1)(πE1.eid(σE1.salary < E2.salaryE1×E2))))\pi_{\text{E.Ename}} (\text{E} \bowtie ((\pi_{\text{E1.eid}} E1) - (\pi_{\text{E1.eid}} (\sigma_{\text{E1.salary < E2.salary}} E1 \times E2))))

我们从内向外拆解:

  • I=πE1.eid(σE1.salary < E2.salaryE1×E2)I = \pi_{\text{E1.eid}} (\sigma_{\text{E1.salary < E2.salary}} E1 \times E2) 是在选择满足条件的行,然后进行笛卡尔积,最后投影出 E1.eid 列。它是从笛卡尔积中选择 E1 的薪水小于 E2 的薪水的行。
  • πE1.eidE1I\pi_{\text{E1.eid}} E1 - I 是从 E1 中删除 I 中的行,然后投影出 E1.eid 列。即找出 没有人薪水比自己高的员工ID(即薪水最高的员工)
  • E(πE1.eidE1I)\text{E} \bowtie (\pi_{\text{E1.eid}} E1 - I) 是进行自然连接,保留所有具有相同属性名及其值的行。即获取薪水最高的员工的 完整记录
  • πE.Ename(EI)\pi_{\text{E.Ename}} (\text{E} \bowtie I) 最后投影出 E.Ename 列。最后投影出员工姓名。
SELECT E.Ename
FROM Employee E
WHERE NOT EXISTS (
SELECT *
FROM Employee E2
WHERE E2.Salary > E.Salary
);
-- 等价于
SELECT E.Ename
FROM Employee E
WHERE E.Salary >= ALL (
SELECT E2.Salary
FROM Employee E2
);

我们可以从内向外分析这个关系代数表达式的步骤,假设:

  1. E1 × E2 (部分)
E1.eid | E1.Salary | E2.eid | E2.Salary
-------|-----------|--------|----------
1 | 5000 | 1 | 5000
1 | 5000 | 2 | 6000
1 | 5000 | 3 | 5500
1 | 5000 | 4 | 6000
2 | 6000 | 1 | 5000
2 | 6000 | 2 | 6000
2 | 6000 | 3 | 5500
2 | 6000 | 4 | 6000
...
  1. σ_E1.salary < E2.salary (E1 × E2)
E1.eid | E1.Salary | E2.eid | E2.Salary
-------|-----------|--------|----------
1 | 5000 | 2 | 6000 ← Alice < Bob
1 | 5000 | 3 | 5500 ← Alice < Carol
1 | 5000 | 4 | 6000 ← Alice < David
3 | 5500 | 2 | 6000 ← Carol < Bob
3 | 5500 | 4 | 6000 ← Carol < David
  1. π_E1.eid (...)
有人薪水比自己高的员工:
eid
---
1 (Alice)
3 (Carol)
  1. 差集
所有员工:{1, 2, 3, 4}
有人比自己高:{1, 3}
差集:{2, 4}
  1. 最终结果
Ename
-----
Bob
David

最后得到:Bob 和 David 的薪水都是 6000,是最高的,两人都被选出。

Let R(A,B,C) and S(D,E,F) be two type compatible relation schemas. Convert the following algebra expressions to SQL (for simplicity, you can omit DISTINCT):

设 R(A,B,C) 和 S(D,E,F) 是两个类型兼容的关系模式。将下列代数表达式转换为 SQL(为简化起见,可省略 DISTINCT):

  1. πA,F(RC=DS)\pi_{A,F}(R \bowtie_{C=D} S)

这一题内层是一个等值连接,当属性 C 的值等于属性 D 的值时,两个表行会连接起来。我们可以使用 JOIN 关键字来实现等值连接。

SELECT A, F
FROM R
JOIN S ON R.C = S.D;
-- 等价于
SELECT A, F
FROM R, S
WHERE R.C = S.D;

对于如下关系模式,* 表示主码:

Sailor (sid*, sname, rating, age);
Boat (bid*, bname, color);
Reservation (sid*, bid*, date*);

请找出其关系代数表达式和SQL语句。

  1. 找到预订所有的船的船员名字

我们分析题目,可以知道:找到预订了所有船的船员名字 -> (双重否定) 不存在一艘船没有被预订。这是一个典型是除法问题,因为存在关键词 “所有”。按照除法的思维,条件是所有的船,被除数就是被预定的船的信息。除出来的结果就是预定了所有的船的记录,再投影出船员名字即可。

  • 所有船: πbid(Boat)\pi_{\text{bid}} (\text{Boat})
SELECT B.bid
FROM Boat B;
  • 所有被预订的船,因为最后要找到船员名字,所以需要留下 sid 去匹配船员信息。我们获取预订关系的(船员ID, 船ID)对: πsid, bid(Reservation)\pi_{\text{sid, bid}} (\text{Reservation})
SELECT R.sid, R.bid
FROM Reservation R
WHERE R.bid = B.bid AND R.sid = S.sid;
  • 执行除法,找出预订了所有船的船员ID:πsid, bid(Reservation)÷πbid(Boat)\pi_{\text{sid, bid}} (\text{Reservation}) \div \pi_{\text{bid}} (\text{Boat})
SELECT B.bid
FROM Boat B
WHERE NOT EXISTS (
SELECT R.sid, R.bid
FROM Reservation R
WHERE R.sid = S.sid
AND R.bid = B.bid
);
  • 与Sailor表连接并投影,并获取船员名字:πsname(Sailor(πsid, bid(Reservation)÷πbid(Boat)))\pi_{\text{sname}} (Sailor \bowtie (\pi_{\text{sid, bid}} (\text{Reservation}) \div \pi_{\text{bid}} (\text{Boat})))
SELECT S.sname
FROM Sailor S
WHERE NOT EXISTS (
SELECT B.bid
FROM Boat B
WHERE NOT EXISTS (
SELECT R.sid, R.bid
FROM Reservation R
WHERE R.sid = S.sid
AND R.bid = B.bid
)
);

梳理这一过程:“不存在某条船,使得该船员没有预订它”

  • 外层NOT EXISTS:找不到反例
  • 内层NOT EXISTS:该船员没有预订某条船
  1. 找到预订所有红船的船员名字

这一题与问题7很相似,除数从”所有船”变为”所有红船”,增加了一层条件。所以此时的除数变成是”所有红船”。

所以第一步我们要找到的是所有红船:

  • 所有红船: πbid(σcolor=’red’(Boat))\pi_{\text{bid}} (\sigma_{\text{color='red'}}(\text{Boat}))
SELECT B.bid
FROM Boat B
WHERE B.color = 'red';
  • 所有预订关系的(船员ID, 船ID)对:πsid, bid(Reservation)\pi_{\text{sid, bid}} (\text{Reservation})
SELECT R.sid, R.bid
FROM Reservation R
WHERE R.bid = B.bid AND R.sid = S.sid;
  • 执行除法,找出预订了所有船的船员ID:πsid, bid(Reservation)÷πbid(σcolor=’red’(Boat))\pi_{\text{sid, bid}} (\text{Reservation}) \div \pi_{\text{bid}} (\sigma_{\text{color='red'}}(\text{Boat}))
SELECT B.bid
FROM Boat B
WHERE B.color = 'red'
AND NOT EXISTS (
SELECT R.sid, R.bid
FROM Reservation R
WHERE R.sid = S.sid
AND R.bid = B.bid
);
  • 与Sailor表连接并投影,并获取船员名字:πsname(Sailor(πsid, bid(Reservation)÷πbid(σcolor=’red’(Boat))))\pi_{\text{sname}} (Sailor \bowtie (\pi_{\text{sid, bid}} (\text{Reservation}) \div \pi_{\text{bid}} (\sigma_{\text{color='red'}}(\text{Boat}))))
SELECT S.sname
FROM Sailor S
WHERE NOT EXISTS (
SELECT B.bid
FROM Boat B
WHERE B.color = 'red'
AND NOT EXISTS (
SELECT R.sid, R.bid
FROM Reservation R
WHERE R.sid = S.sid
AND R.bid = B.bid
)
);

关系代数总结#

总结一下关系代数转换技巧:

操作类型关系代数SQL关键语法转换说明
选择σ条件(R)\sigma_{\text{条件}}(R)WHERE根据条件筛选行,直接将关系代数的条件表达式放入WHERE子句
投影πA,B,C(R)\pi_{A,B,C}(R)SELECT A, B, C选择指定列,将下标中的属性列表放入SELECT子句
RSR \cup SUNION合并两个查询结果,自动去重;需保留重复则用UNION ALL
RSR - SEXCEPT返回在R中但不在S中的元组;部分数据库用MINUS
RSR \cap SINTERSECT返回同时存在于R和S中的元组
笛卡尔积R×SR \times SCROSS JOIN,两表的所有组合,FROM子句用逗号或CROSS JOIN
θ-连接RθSR \bowtie_{\theta} SJOIN ... ON θ笛卡尔积+条件筛选,θ条件放入ON子句或WHERE子句
等值连接RA=BSR \bowtie_{A=B} SJOIN ... ON A = Bθ-连接的特例,连接条件为等值比较
自然连接RSR \bowtie SNATURAL JOIN自动匹配同名属性并去重列;或手动指定公共属性等值连接
除法R÷SR \div SNOT EXISTS 嵌套
GROUP BY ... HAVING COUNT
双重否定法:不存在S中某元组使得R中不存在匹配
计数法:按R的非公共属性分组,计数等于S的元组数

常见的一些需要嵌套查询的情况:

操作核心思路关键词提示
除法→双重否定找不到反例NOT EXISTSNOT EXISTS
除法→计数法匹配数等于除数总数GROUP BY + HAVING COUNT(*) = ...
差集→子查询不在另一个集合中NOT INNOT EXISTS
交集→子查询在另一个集合中INEXISTS

函数依赖#

在实际数据库的应用中,很容易出现存储重复数据、冗余数据、数据不一致、数据丢失等问题。

函数依赖 (Function Dependency, FD) 是一种重要的形式工具和设计理念,能够精确地检测并描述上述的一些问题。

Definition

关系模式 RR 的两个属性子集 XXYY 之间的函数依赖记作 XYX\rightarrow Y,表示 YY 属性的值由 XX 属性决定。

若在 XX 上取值相同的元组在 YY 上也相等,对于 RR 中的任意两个元组 t1t_1t2t_2,如果有 t1.x=t2.xt_1.x = t_2.x,则 t1.y=t2.yt_1.y = t_2.y,则说明 FD XYX\rightarrow Y 成立,XX 决定 YY

例如:

R:
| A | B | C | D |
-----+----+----+-----
| a1 | b1 | c1 | d1 |
| a1 | b2 | c1 | d2 |
| a2 | b4 | c2 | d1 |
| a3 | b1 | c3 | d3 |
| a4 | b2 | c1 | d4 |
| a4 | b3 | c1 | d5 |

我们可以发现 RR 中存在 FD ACA\rightarrow C,因为当 t1.A=t2.At_1.A = t_2.A 时,t1.C=t2.Ct_1.C = t_2.C。注意,其中存在有 (a4,c1)(a_4,c_1) 这样的元组,因为这个 对应关系和下标无关

但是,FD ACA\rightarrow C 不代表 CAC\rightarrow A,因为存在 (c1,a1),(c1,a4)(c_1,a_1), (c_1,a_4) 这样的对应关系,所以函数依赖反过来不一定成立。

若任意元组中两行的 XX 取值均不同,则 XYX\rightarrow Y 称为平凡保持 (Trivial Preserved)

此时若有:

  • FD XY,YXX \rightarrow Y, Y \subseteq X,则该 FD 称为 平凡函数依赖 (Trivial FD)。例如,{name,age}{name}\{name, age\} \rightarrow \{name\},这样的FD 称为平凡函数依赖。
  • FD XY,Y⊈XX \rightarrow Y, Y \not \subseteq X,则该 FD 称为 非平凡函数依赖 (Non-Trivial FD)。

例如:

| sid | sname | cid | cname |
------+------+------+---------
| 1 | A | 1 | C1 |
| 2 | B | 1 | C1 |
| 3 | C | 2 | C2 |
| 4 | D | 2 | C2 |

其中,FD {sid, sname}sname\text{\{sid, sname\}} \rightarrow \text{sname} 是一个平凡的函数依赖,因为 sname{sid, sname} 的一个子集。而 FD sidcname\text{sid} \rightarrow \text{cname} 则是一个非平凡的函数依赖。不能说 cidsname\text{cid} \rightarrow \text{sname} 是平凡的,因为这个没有实际意义,非函数依赖。

超码和候补码#

我们已经知道了函数依赖(FD)描述的是属性之间的”决定”关系。那么,在一个关系(表)中,我们需要找出哪些属性组合能唯一标识每一行记录。这就是我们之前学到的码 (Key) 的定义。

  • 超码 (Super Key): 能唯一标识元组的属性集合。若 α\alphaRR 的超码,则 αR\alpha \rightarrow R,这里 RR 包含了它的所有属性的集合,表示超码能推导出所有属性,即唯一标识每个元组。
  • 候补码 (Candidate Key): 候补码是 最小 的超码,满足超码定义的属性集合,且不能再通过其他属性组合得到。

例如:

R:
| A | B | C | D |
-----+----+----+-----
| a1 | b2 | c1 | d2 |
| a2 | b2 | c1 | d2 |
| a1 | b4 | c3 | d3 |
| a4 | b4 | c3 | d4 |

此时存在这样的函数依赖:ACR\text{AC} \rightarrow \text{R}, ADR\text{AD} \rightarrow \text{R}, ACDR\text{ACD} \rightarrow \text{R}

  • ACR\text{AC} \rightarrow \text{R}: 此时 AC 中每一个元组均唯一 {a1c1, a2c1, a1c3, a4c3},它们满足任何 AC具有的值相等的元组,在 R 中的值都相等,这一条件满足超码定义,故 AC 为超码。若删除 AC 后,它们 均无法唯一标识每个元组,则 AC 是候补码。
  • ADR\text{AD} \rightarrow \text{R}: 同理,AD 中每一个元组均唯一 {a1d2, a2d2, a1d3, a4d4},它们满足任何 AD具有的值相等的元组,在 R 中的值都相等,这一条件满足超码定义,故 AD 为超码。若删除 AD 后,它们 均无法唯一标识每个元组,则 AD 是候补码。
  • ACDR\text{ACD} \rightarrow \text{R}: 同理,ACD 中每一个元组均唯一 {a1d2, a2d2, a1d3, a4d4},它们满足任何 ACD具有的值相等的元组,在 R 中的值都相等,这一条件满足超码定义,故 ACD 为超码。但 ACAD 组合均满足超码定义,故 ACD 不是候选码。

我们把一个关系的所有函数依赖的集合称为关系 R函数依赖集 (Function Dependency set),记为 FF。例如:F={AB,AC,BD}F = \{A \rightarrow B, A \rightarrow C, B \rightarrow D\}

如果说一个函数依赖 fFf \notin F,且能被 FF 中的函数依赖所推导出来,则称 ffFF 蕴含 (Imply)。例如,f=ADf = A \rightarrow DFF 蕴含,因为 AB,BDA \rightarrow B, B \rightarrow D 表示 AA能确定 BBBB 可以确定 DD,故 AA 可以确定 DD

闭包#

从函数依赖集 FF 出发,通过推理规则能够推导出的其他函数依赖(包括F本身)。我们将推导结果的集合其称为 FF 的闭包 (F+F^+ Closure),记作 F+F^+。它是所有可以从 FF 推导出来的函数依赖的集合

推导 FF 闭包的核心工具是 Armstrong 公理,它有如下的规则:

  1. 自反律 (Reflexivity): 如果 YXY ⊆ X,则 XYX → Y。例如 {A,B,C}{A,C}\{A, B, C\} → \{A, C\}
  2. 增广律 (Augmentation): 如果 XYX → Y,则 XZYZXZ → YZ(对任意属性集 ZZ)。例如:ABA → B,则 ACBCAC → BC
  3. 传递律 (Transitivity): 如果 XY,YZX → Y, Y → Z,则 XZX → Z。例如:ABA → BBCB → C,则 ACA → C

还有三个派生规则:

  1. 合并律 (Union): 如果 XYX → YXZX → Z,则 XYZX → YZ
  2. 分解律 (Decomposition): 如果 XYZX → YZ,则 XYX → YXZX → Z
  3. 部分传递律 (Partial Transitivity): 如果 XYX → YWYZWY → Z,则 XWZXW → Z

我们一般使用前三条规则就可以找到 FF 的闭包了。

例如,

F={AB,BC}F = \{A → B, B → C\}, R={A,B,C}R = \{A, B, C\},求FF 的闭包 F+F^+

  1. 我们先添加 RR 属性自身:F+={AA,BB,CC}F^+ = \{A \rightarrow A, B \rightarrow B, C \rightarrow C\}
  2. 开始逐步运用三大定律和已有的 FFF+={,AC(传递),ABB(自反,ACBC(增广)),}F^+ = \{\dots, A \rightarrow C (\text{传递}), AB \rightarrow B (\text{自反}, AC \rightarrow BC (\text{增广})), \dots\}

最后得到结果:

F+={AA,AB,AC,AAB,AAC,ABC,AABC,BB,BC,BBC,CC,ABA,ABB,ABC,ABAB,ABAC,ABBC,ABABC,ACA,ACB,ACC,ACAB,ACAC,ACBC,ACABC,BCB,BCC,BCBC,ABCA,ABCB,ABCC,ABCAB,ABCAC,ABCBC,ABCABC}\begin{aligned} F^+ = \{ &A \to A, \quad A \to B, \quad A \to C, \quad A \to AB, \quad A \to AC, \quad A \to BC, \quad A \to ABC, \\ &B \to B, \quad B \to C, \quad B \to BC, \\ &C \to C, \\ &AB \to A, \quad AB \to B, \quad AB \to C, \quad AB \to AB, \quad AB \to AC, \quad AB \to BC, \quad AB \to ABC, \\ &AC \to A, \quad AC \to B, \quad AC \to C, \quad AC \to AB, \quad AC \to AC, \quad AC \to BC, \quad AC \to ABC, \\ &BC \to B, \quad BC \to C, \quad BC \to BC, \\ &ABC \to A, \quad ABC \to B, \quad ABC \to C, \quad ABC \to AB, \quad ABC \to AC, \quad ABC \to BC, \quad ABC \to ABC \} \end{aligned}

属性闭包 (Attribute Closure) 是 F+F^+ 的一个子集 X+X^+,表示从属性集 XX 出发,利用函数依赖集 FF 中的规则,能够直接或间接确定(推导出)的所有属性的集合

例如:

F={AB,BC}F = \{A → B, B → C\},求属性闭包 A+A^+

  1. 初始化自身属性:A+={A}A^+ = \{A\}
  2. 遍历每个函数依赖若属性未被加入,则加入该属性并计算其闭包
    • 对于 ABA \to BBB 不在闭包中,则加入 A+={A}{B}={AB}A^+ = \{A\} \cup \{B\}= \{AB\}
    • 计算 B+B^+: B+={B}{C}={BC}B^+ = \{B\} \cup \{C\} = \{BC\},则加入 A+={AB}{BC}={ABC}A^+ = \{AB\} \cup \{BC\} = \{ABC\}
    • 计算 C+:C^+: C+={C}C^+ = \{C\},则加入 A+={ABC}A^+ = \{ABC\}

最后,我们得到了 A+={ABC}A^+ = \{ABC\}

还有一个组合属性集的例子,给定:

R(A, B, C, D), F = {AB → C, C → D}

计算 {A}+\{\text{A}\}^+:

初始化:result = {A}
第1轮:
- AB → C:{A, B} ⊄ {A} ✗(缺B)
- C → D:C ⊄ {A} ✗
结果:{A}⁺ = {A}

计算 {A,B}+\{\text{A,B}\}^+

初始化:result = {A, B}
第1轮:
- AB → C:{A, B} ⊆ {A, B} ✓ → result = {A, B, C}
- C → D:C ⊆ {A, B, C} ✓ → result = {A, B, C, D}
第2轮:
- 无新属性可加
结果:{A, B}⁺ = {A, B, C, D} = R

函数依赖例题和总结#

函数依赖例题#

  1. 在实际情况中,我们一般只会看到表格的部分记录,因此我们一般会讨论函数 ff 是否满足函数依赖关系,请你根据下面的元组,给出5个不满足函数依赖关系的 ff
| A | B | C |
| - | - | - |
| a1 | b1 | c1 |
| a1 | b1 | c2 |
| a2 | b1 | c1 |
| a2 | b1 | c3 |

根据函数依赖的定义:f={X}{Y}f = \{X\} \to \{Y\} 满足函数依赖关系,当且仅当 ff 中的属性值 XXYY 对于任意元组 t1,t2t_1,t_2 满足 t1[X]=t2[X]t_1[X] = t_2[X] 时,t1[Y]=t2[Y]t_1[Y] = t_2[Y]

我们可以举出如下不满足函数依赖关系的例子:

  • f={B}{A}f = \{B\} \to \{A\}
  • f={C}{A}f = \{C\} \to \{A\}
  • f={B}{C}f = \{B\} \to \{C\}
  • f={A}{C}f = \{A\} \to \{C\}
  • f={A,B}{C}f = \{A,B\} \to \{C\}
  1. R=Title, Theater, CityR = \text{Title, Theater, City} 其中,Title 是电影的名字,Theater 是电影院的名字,City 是电影院所在的城市。

现在有如下限制:

  • 不同的城市不会存在两个相同名字的电影院。
  • 一个城市中的不同电影院不会播放相同的电影。
  • 一个电影院可以播放多个电影。

请你找到限制中隐藏的函数依赖关系。

我们可以逐条转化这些自然语言限制:

  • “不同的城市不会存在两个相同名字的电影院”: Theater → City
  • “一个城市中的不同电影院不会播放相同的电影”: 在同一个城市,电影和电影院是一一对应的
  • “一个电影院可以播放多个电影”: 说明 Theater 不能唯一确定 Title

那我们可以推导出:

  • Theater → City: 知道电影院,就可以知道城市
  • City, Title → Theater: 同一城市 + 同一电影 → 必定是同一个电影院

我们可以逐一计算其属性闭包:

  • Title+\text{Title}^+:

    • 对于 Theater → City, Theater⊈Title\text{Theater} \not \subseteq \text{Title}
    • 对于 City, Title → Theater, {City, Title}⊈Title\{\text{City, Title}\} \not \subseteq \text{Title}

    所以 Title+={Title}\text{Title}^+ = \{\text{Title}\}

  • Theater+\text{Theater}^+:

    • 对于 Theater → CityTheater{Theater}\text{Theater} ⊆ \text{\{Theater\}},所以 Theater+={Theater, City}\text{Theater}^+ = \{\text{Theater, City}\}
    • 对于 City, Title → Theater, {City, Title}⊈{Theater, City}\{\text{City, Title}\} \not \subseteq \{\text{Theater, City}\}

    所以 Theater+={Theater,City}\text{Theater}^+ = \{\text{Theater}, \text{City}\}.

  • City+\text{City}^+:

    • 对于 Theater → City, Theater⊈{City}\text{Theater} \not \subseteq \{\text{City}\}
    • 对于 City, Title → Theater, {City, Title}{City}\{\text{City, Title}\} \subseteq \{\text{City}\}

    所以 City+={City}\text{City}^+ = \{\text{City}\}.

观察一下这两个函数依赖,我们可以发现 FD2 City, Title → Theater 可以结合 FD1 Theater → City 运用传递律得到:City, Title → City,这是一个平凡FD。同理,可以推导出 City, Title → Title,所以 City, Title → Title, Theater, City = R,此时 City, Title 是一个超码,根据三个属性的闭包均不等于 RR 可知,它是一个候选码。

此外,我们对 FD1 运用增广律,可以得到 Title, Theater → Title, City,再加上自身属性 Title, Theater,它也是一个候选码。

当然我们也可以使用比较规范的方法求候选码,该方法记录在该章节的总结部分:函数依赖总结

先找到四种属性分类:

  • L (只在 FD 左侧出现): Title\text{Title}
  • R (只在 FD 右侧出现): \emptyset
  • N (不在 FD 中出现): \emptyset
  • LR (既在 FD 左侧又在 FD 右侧出现): City, Theater\text{City, Theater}

所以,L 的属性 Title\text{Title} 是候选码的一部分。接下来组合验证:

  • TitleCity\text{Title} \cup \text{City}: 求它的属性闭包,很明显可以得到 (Title, City)+=R\text{(Title, City)}^+ = R,所以 (Title, City)\text{(Title, City)} 是一个候选码。
  • TitleTheater\text{Title} \cup \text{Theater}: 同上,(Title, Theater)+=R\text{(Title, Theater)}^+ = R,所以 (Title, Theater)\text{(Title, Theater)} 也是一个候选码。
  1. 我们想要创新一个数据库去存储银行的账户信息 Accounts (A), 分支信息 Branches (B) 和 客户信息 Customers (C),并满足下面给出的限制。
  • 一个账户不会被多个客户共享。
  • 两个不同的分支不会拥有相同的账户。
  • 一个用户在一个支行只能拥有一个账户,但可以在不同的支行拥有不同的账户。

请你找到限制中隐藏的函数依赖关系。

解析这些限制关系:

  • “一个账户不会被多个客户共享”: 这意味着一个账户只能对应一个客户,A → C
  • “两个不同的分支不会拥有相同的账户”: 这意味着一个账户只能对应一个分支,A → B
  • “一个用户在一个支行只能拥有一个账户,但可以在不同的支行拥有不同的账户”: 这意味着一个用户和一个支行能确定一个账户,{C, B} → A

所以我们可以推导出:

  • A → C: 账户只能对应一个客户
  • A → B: 账户只能对应一个分支
  • {C, B} → A: 一个用户和一个支行能确定一个账户

接下来我们来找出该数据库的候选码,首先找出单属性闭包:

  • A+\text{A}^+:

    • A → C: {A}{A}\{\text{A}\} \subseteq \{\text{A}\},所以 A+={A, C}\text{A}^+ = \{\text{A, C}\}.
    • A → B: 同上,A+={A, B, C}\text{A}^+ = \{\text{A, B, C}\}
    • {C, B} → A: {C, B}⊈{A}\{\text{C, B}\} \not \subseteq \{\text{A}\},没有属性需要加入。

    所以 A+={A, B, C}=R\text{A}^+ = \{\text{A, B, C}\} = R.

  • B+\text{B}^+:

    • A → C: {A}⊈{B}\{\text{A}\} \not \subseteq \{\text{B}\}
    • A → B: {A}⊈{B}\{\text{A}\} \not \subseteq \{\text{B}\}
    • {C, B} → A: {C, B}⊈{B}\{\text{C, B}\} \not \subseteq \{\text{B}\}

    所以 B+={B}\text{B}^+ = \{\text{B}\}.

  • C+\text{C}^+:

    • A → C: {A}⊈{C}\{\text{A}\} \not \subseteq \{\text{C}\}
    • A → B: {A}⊈{C}\{\text{A}\} \not \subseteq \{\text{C}\}
    • {C, B} → A: {C, B}⊈{C}\{\text{C, B}\} \not \subseteq \{\text{C}\}

    所以 C+={C}\text{C}^+ = \{\text{C}\}.

可以发现,A+=RA^+ = R 是一个候选码。接下来考虑两个属性的闭包,这里省略了。

可以发现,{C, B}A,AR    {C, B}R\text{\{C, B\}} \to A, A \to R \implies \text{\{C, B\}} \to R,它的两个子属性闭包不等于 RR,所以 {C, B}\text{\{C, B\}} 也是一个候选码。

  1. R={A,B,C}R = \{A, B, C\} 我们不知道它的码是什么,如果 AA 是一个候选码,你会怎么样去检测它?使用 SQL 语句去检测。

如果 ABA \to B 是一个函数依赖,请你使用 SQL 语句去检测它。

回顾码的定义: 码是一个能够唯一标识一个元组的属性集合。码属性的数量等于所有元组的数量。

那么我们使用 GROUP BY 得到的结果,每一组都是一个元组。

SELECT A
FROM R
GROUP BY A
HAVING COUNT(*) > 1;
  • 如果结果是空,则 AA 是一个候选码。
  • 如果结果不为空,则 AA 不是一个候选码。

对于 ABA \to B,说明 AA 属性能唯一确定 BB 属性的值。那么我们只需要按照 AA 进行分组,其中每个组只有一个 BB 属性值则关系成立。

SELECT A
FROM R
GROUP BY A
HAVING COUNT(DISTINCT B) > 1;
  • 如果结果是空,则 ABA \to B 是 FD。
  • 如果结果不为空,则 ABA \to B 是 FD。
  1. 对于 R={X,Y,U,V,W}R = \{X, Y, U, V, W\},有如下的函数依赖:XY,{U,V}W,VXX \to Y, \{U, V\} \to W, V \to X,找到每个单属性闭包。
  1. X+\text{X}^+:
    • X → Y: {X}{X}\{\text{X}\} \subseteq \{\text{X}\}, 所以 X+={X, Y}\text{X}^+ = \{\text{X, Y}\}.
    • {U, V} → W: {U, V}⊈{X, Y}\{\text{U, V}\} \not \subseteq \{\text{X, Y}\}, 没有属性需要加入。
    • V → X: {V}⊈{X, Y}\{\text{V}\} \not \subseteq \{\text{X, Y}\}, 没有属性需要加入。

所以 X+={X, Y}\text{X}^+ = \{\text{X, Y}\}.

  1. Y+\text{Y}^+:
    • X → Y: {X}⊈{Y}\{\text{X}\} \not \subseteq \{\text{Y}\}, 没有属性需要加入。
    • {U, V} → W: {U, V}⊈{Y}\{\text{U, V}\} \not \subseteq \{\text{Y}\}, 没有属性需要加入。
    • V → X: {V}⊈{Y}\{\text{V}\} \not \subseteq \{\text{Y}\}, 没有属性需要加入。

所以 Y+={Y}\text{Y}^+ = \{\text{Y}\}.

  1. U+\text{U}^+:
    • X → Y: {X}⊈{U}\{\text{X}\} \not \subseteq \{\text{U}\}, 没有属性需要加入。
    • {U, V} → W: {U, V}⊈{U}\{\text{U, V}\} \not \subseteq \{\text{U}\}, 没有属性需要加入。
    • V → X: {V}⊈{U}\{\text{V}\} \not \subseteq \{\text{U}\}, 没有属性需要加入。

所以 U+={U}\text{U}^+ = \{\text{U}\}.

  1. V+\text{V}^+:
    • X → Y: {X}⊈{V}\{\text{X}\} \not \subseteq \{\text{V}\}, 没有属性需要加入。
    • {U, V} → W: {U, V}⊈{V}\{\text{U, V}\} \not \subseteq \{\text{V}\}, 没有属性需要加入。
    • V → X: {V}{V}\{\text{V}\} \subseteq \{\text{V}\}, 所以 V+={V, X}\text{V}^+ = \{\text{V, X}\}.
    • 重新检测 X → Y: {X}{V, X}\{\text{X}\} \subseteq \{\text{V, X}\},所以 V+={V, X, Y}\text{V}^+ = \{\text{V, X, Y}\}.
    • 重新检测 {U, V} → W: {U, V}⊈{V, X, Y}\{\text{U, V}\} \not \subseteq \{\text{V, X, Y}\}, 没有属性需要加入。
    • 重新检测 V → X: {V}{V, X, Y}\{\text{V}\} \subseteq \{\text{V, X, Y}\},但 XX 已经在闭包中,无需加入。

所以 V+={V, X, Y}\text{V}^+ = \{\text{V, X, Y}\}.

  1. W+\text{W}^+:
    • X → Y: {X}⊈{W}\{\text{X}\} \not \subseteq \{\text{W}\}, 没有属性需要加入。
    • {U, V} → W: {U, V}⊈{W}\{\text{U, V}\} \not \subseteq \{\text{W}\}, 没有属性需要加入。
    • V → X: {V}⊈{W}\{\text{V}\} \not \subseteq \{\text{W}\}, 没有属性需要加入。

所以 W+={W}\text{W}^+ = \{\text{W}\}.

函数依赖总结#

我们来总结一下函数依赖相关的问题的步骤和技巧:

函数依赖 (Functional Dependency, FD):XYX → Y 如果知道了 XX 的值,就能唯一确定 YY 的值。我们还有如下重要概念:

概念定义示例
平凡依赖Y ⊆ X 的函数依赖{A, B} → A
非平凡依赖Y ⊄ X 的函数依赖A → B
推导函数依赖Y 依赖于 X,但不依赖于 X 的任何真子集{学号,课程} → 成绩

常见的题型是:根据数据库限制找函数依赖和候补码。

常见的有如下关键词表示 函数依赖关系

现实数据库限制含义FD模式实例句子
X has a unique YX有唯一的YX → YCourse has a unique course code
X has only one YX只有一个YX → YEmployee has only one manager
for each X, there is only one Y对每个X只有一个YX → YFor each student, there is only one major
each X has a fixed Y每个X有固定的YX → YEach course has a fixed credit value
given X, we can determine Y给定X可确定YX → YGiven ISBN, we can determine title

下面表格总结了一些 非函数依赖关系的标识:

现实数据库限制含义FD模式实例句子
X can have multiple YX可以有多个YX ↛ YProfessor can have multiple courses
X may have many YX可能有多个YX ↛ YStudent may have many hobbies
X has several YX有多个YX ↛ YAuthor has several books
X and Y are many-to-manyX和Y是多对多X ↛ Y, Y ↛ XStudents and courses are many-to-many
Y varies independently of XY独立于X变化X ↛ YSupplier price varies independently of product
Y is independent of XY独立于XX ↛ YBirth date is independent of salary
multiple X can share same Y多个X可共享同一YX ↛ YMultiple employees can share same office

我们需要找到候选码,先回顾码的定义:

  • Superkey(超码):能唯一标识元组的属性集
  • Candidate Key(候选码):最小超码(minimal superkey)

我们可以将属性分为四类:

类别定义特点
L(Left)只出现在 FD 左边必然在候选码中
R(Right)只出现在 FD 右边不可能在候选码中
N(Neither)两边都不出现必然在候选码中
LR(Both)两边都出现可能在候选码中

对于关系 R(A1,A2,,An)R (A_1,A_2,\dots,A_n), 函数依赖集 FF:

  1. Step 1: 属性分类:计算 L, R, N, LR 类属性
  2. Step 2: 初始候选码Base=LN\text{Base} = L \cup N (这些属性必须在候选码中)
  3. Step 3: 检验 Base: 计算 Base+\text{Base}⁺,如果 Base+=R\text{Base}⁺ = R,则 Base 是唯一候选码。否则添加 LR 属性,对 LR 类属性的每个子集 S:计算 (BaseS)+(\text{Base} \cup S)⁺,如果等于所有属性则是候选码。

例如:

关系:R(A, B, C, D, E)
FD:F = {A → B, BC → E, ED → A}
Step 1: 分类
L: {C, D} (只在左边)
R: {} (只在右边)
N: {} (两边都不出现)
LR: {A, B, E} (两边都出现)
Step 2: Base = {C, D}
Step 3: 计算 {C,D}⁺
{C,D}⁺ = {C,D} (不能推导更多属性)
不是候选码,需要添加 LR 属性
Step 4: 尝试添加 A 或 E
- {C,D,A}⁺ = {C,D,A,B,E} ✓ 是超码
检查最小性:
- {C,D}⁺ = {C,D} ✗
- {C,A}⁺ = {C,A,B} ✗
- {D,A}⁺ = {D,A,B} ✗
→ {C,D,A} 是候选码
- {C,D,B}⁺ = {C,D,B,E,A} ✓ 是超码
检查最小性:
- {B,C}⁺ = {B,C,E} ✗
- {B,D}⁺ = {B,D} ✗
→ {C,D,B} 是候选码
- {C,D,E}⁺ = {C,D,E,A,B} ✓ 是超码
检查最小性:
- {C,E}⁺ = {C,E} ✗
- {D,E}⁺ = {D,E,A,B} ✗
→ {C,D,E} 是候选码
结论:候选码为 {CDA}, {CDB}, {CDE}

对于求属性闭包的问题,我们一般采取以下示例中的步骤:

有一个数据库:R(A,B,C,D,E)R(A,B,C,D,E), F=ABC,CDE,BD,EAF = {A → BC, CD → E, B → D, E → A},求 {A}+\{A\}⁺

  1. 先取自反: {A}+={A}\{A\}⁺ = \{A\}
  2. 检查 ABCA → BC: 发现 AA 属于当前闭包,加入后续属性到闭包中:{A}+={A,B,C}\{A\}⁺ = \{A, B, C\}
  3. 可以直接检查 BDB → D,因为 BB 属于当前闭包: {A}+={A,B,C,D}\{A\}⁺ = \{A, B, C, D\}
  4. 检查 CDECD → E,因为此时 CDCD 均在闭包中,需要重新检查其他函数依赖: {A}+={A,B,C,D,E}\{A\}⁺ = \{A, B, C, D, E\}
  5. 检查 EAE → A,无元素可以添加: {A}+={A,B,C,D,E}\{A\}⁺ = \{A, B, C, D, E\}

结果:{A}+={A,B,C,D,E}\{A\}⁺ = \{A,B,C,D,E\}AA 是候选码。


范式化#

范式#

范式 (Normal Form, NF) 是在数据库设计中遵循的一系列规则,以确保数据结构的优化。

数据库中提出了 4 种常见的范式:1NF, 2NF, 3NF, BCNF

  • 1NF:每个属性都是 原子的(atomic),不可再分。即不存在多值属性或嵌套属性。
  • 2NF:满足 1NF,并且非主属性 (除主码之外的属性) 必须完全依赖于候选码。即不存在非主属性的部分依赖。
  • 3NF:满足 2NF,并且非主属性不能有传递依赖。即 3NF 要求每一个非主属性都与候选码直接相关,而不是间接相关。
  • BCNF (Boyce-codd Normal Form):满足 3NF,并且满足 RR 中存在一个 非平凡依赖 XAX \to A (A⊈XA \not \subseteq X), 且 XXRR超码,则 RR 属于 BCNF。

其中 2NF 和 3NF 提到的两个依赖的概念:

  • 部分依赖 (Partical Dependency):一个函数依赖关系形如:{X,Y}Z\{X,Y\} → Z,若存在子集 SS (XXYY),使得 SZS → Z,则称 ZZ 部分依赖{X,Y}\{X,Y\}
  • 传递依赖 (Transitive Dependency):函数依赖关系形如:AB,BCA \to B, B \to C,则称 ACA \to C传递依赖

我们主要关注 3NF 和 BCNF,因为 3NF 和 BCNF 是数据库中最常用的范式。

重新关注这两个范式,我们可以得到:

  • BCNF: 若RR 属于 BCNF,对于任意 {XA}F\{X \to A\} \subseteq F,满足:
    1. XAX \to A平凡依赖,或
    2. XXRR超码
  • 3NF: 若 RR 属于 3NF,对于任意 {XA}F\{X \to A\} \subseteq F,满足:
    1. XAX \to A平凡依赖,或
    2. XXRR超码,或
    3. AA某个候选键的一部分

例如:

R=(A,B,C),F={AB,BC},key={A}R = (A,B,C), F = \{A → B, B → C\}, key = \{A\},判断是否属于 BCNF。

  • 对于 {AB}\{A \to B\}AARR 的码;
  • 对于 {BC}\{B \to C\},不存在平凡依赖 (C⊈BC \not \subseteq B) 且 BB 不为码,所以 RR 不属于 BCNF。

R=(A,B,C,D,F),F={AEBCD,DA},key={AE,DE}R = (A,B,C,D,F), F = \{AE → BCD, D → A\}, key = \{AE,DE\},判断是否属于 3NF。

  • 对于 {AEBCD}\{AE \to BCD\}AEAERR 的码;
  • 对于 {DA}\{D \to A\},不存在平凡依赖 (A⊈DA \not \subseteq D),DD 不为码,AA 是码 AEAE 的一部分。

所以 RR 属于 3NF。

分解#

我们知道怎么判断一个关系模式 RR 是否属于某个范式后,对于不合格的关系模式,我们可以进行 分解 (Decomposition),将关系模式分解为多个关系模式,使得这些关系模式都满足该范式。

对于 3NF 而言,我们有两种常见的分解方法:

  1. 无损连接分解 (Lossless Join Decomposition): 将一个关系模式经过分解后通过自然连接能恢复原关系。即,R    R1R2,R1R2=RR \implies R_1 \land R_2, R_1 \Join R_2 = R。根据这个条件,我们还可以推导出:两个分解关系的公共属性 (R1R2R_1 \cap R_2) 必须是其中 至少一个关系 (R1R_1R2R_2) 的超码。这意味着:R1R2R1R2R_1 ∩ R_2 → R_1 - R_2R1R2R2R1R_1 ∩ R_2 → R_2 - R_1
  2. 保持依赖分解 (Dependency Preserving Decomposition): 将一个关系模式分解为多个关系模式,使得这些关系模式满足依赖关系。即,R    R1R2,(F1F2)+=F+R \implies R_1 \land R_2, (F_1 \cup F_2)^+ = F^+

例如:

R(sid,sname,major)R (\text{sid}*, \text{sname}, \text{major}),分解为 R1(sid,sname),R2(sid,major)R_1 (\text{sid}*, \text{sname}), R_2 (\text{sid}*, \text{major}),请判断是否为无损连接分解。若分解为 R3(sid,sname),R4(sname,major)R_3 (\text{sid}*, \text{sname}), R_4 (\text{sname}, \text{major}),请判断是否为无损连接分解。

对于 R1R_1R2R_2,我们计算 R1R2=(sid,sname,major)=RR_1 \Join R_2 = (\text{sid}*, \text{sname}, \text{major}) = R,所以它是一个无损连接分解。

对于 R3R_3R4R_4,我们计算 R3R4=(sid,sname,major)R_3 \Join R_4 = (\text{sid}*, \text{sname}, \text{major}),但是自然连接的条件 sname\text{sname} 并非 RR 的候选码,连接的数据可能不一致,所以它不是一个无损连接分解。

R(sid,sname,major)R (\text{sid}*, \text{sname}, \text{major}),若 F={sidsname,snamemajor}F = \{\text{sid} \to \text{sname}, \text{sname} \to \text{major}\}。分解为 R1(sid,sname),R2(sid,major)R_1 (\text{sid}*, \text{sname}), R_2 (\text{sid}*, \text{major}),请判断是否为保持依赖分解。若分解为 R3(sid,sname),R4(name,major)R_3 (\text{sid}*, \text{sname}), R_4 (\text{name}, \text{major}),请判断是否为保持依赖分解。

对于 R1R_1R2R_2F1={sidsname}F_1 = \{ \text{sid} \to \text{sname} \}F2={sidmajor}F_2 = \{ \text{sid} \to \text{major} \}(F1F2)={sidsname,sidmajor}(F_1 \cup F_2) = \{\text{sid} \to \text{sname}, \text{sid} \to \text{major} \}, F={sidsname,snamemajor}F = \{ \text{sid} \to \text{sname}, \text{sname} \to \text{major} \}(F1F2)+F+(F_1 \cup F_2)^+ \not = F^+,所以它不是一个保持依赖分解。

对于 R3R_3R4R_4F3={sidsname}F_3 = \{ \text{sid} \to \text{sname} \}F4={snamemajor}F_4 = \{ \text{sname} \to \text{major} \}(F3F4)={sidsname,snamemajor}(F_3 \cup F_4) = \{ \text{sid} \to \text{sname}, \text{sname} \to \text{major} \}, F={sidsname,snamemajor}F = \{ \text{sid} \to \text{sname}, \text{sname} \to \text{major} \}(F3F4)+=F+(F_3 \cup F_4)^+ = F^+,它是一个保持依赖分解。

对于 BCNF,我们可以使用无损连接分解,但是 不能使用保持依赖分解。我们一般将 BCNF 分解称为 规范化 (Normalization)。

下面是 BCNF 分解的算法步骤:

输入:R, F
输出:BCNF分解
Algorithm (递归):
If R 不在 BCNF:
找一个违反的 FD (X→Y),其中 X 不是超码
分解为:
R1 = X ∪ Y
R2 = R - (Y - X)
递归分解 R1 和 R2
Else:
返回 R

我们通过一个例子来解析这个过程:

R=(A,B,C,D,E),F={AB,AD,CE}R = (A,B,C,D,E), F = \{A → B, A → D, C → E\}Key={AC}Key = \{AC\},请判断 RR 是否为 BCNF,若不是,请分解为 BCNF。

我们先判断其是否为 BCNF:

  • 对于 {AB}\{A \to B\}AA 不是 RR 的码,违反 BCNF 条件。
  • 对于 {AD}\{A \to D\}AA 不是 RR 的码,违反 BCNF 条件。
  • 对于 {CE}\{C \to E\}CC 不是 RR 的码,违反 BCNF 条件。

所以 RR 不属于 BCNF。

接下来开始执行算法去分解它为 BCNF:

  1. 分解 R=(A,B,C,D,E)R = (A,B,C,D,E):选择 {AB}\{A \to B\},分解为 R1=(AB)=(A,B)R_1 = (A \cup B) = (A, B)R2=R(BA)=(A,C,D,E)R_2 = R - (B - A) = (A, C, D, E). 接下来,递归分解 R1R_1R2R_2
    1. 对于 R1R_1,投影到 R1R_1 的 FD 为:ABA \to BA+={A,B}=R1A⁺ = \{A, B\} = R_1R1R_1 满足 BCNF 条件。
    2. 对于 R2R_2,投影到 R2R_2 的 FD 为:{AD,CE}\{A \to D, C \to E\},对于 {AD}\{A \to D\}AA 不是 R2R_2 的码 ACAC,违反 BCNF 条件;对于 {CE}\{C \to E\}CC 不是 RR 的码,违反 BCNF 条件。R2R_2 不属于 BCNF,需要继续分解。
  2. 分解 R2=(A,C,D,E)R_2 = (A, C, D, E):选择 {AD}\{A \to D\},分解为 R3=(AD)=(A,D)R_3 = (A \cup D) = (A, D)R4=R2(DA)=(A,C,E)R_4 = R_2 - (D - A) = (A, C, E).
    1. 对于 R3R_3,投影到 R3R_3 的 FD 为:ADA \to DA+={A,D}=R3A⁺ = \{A, D\} = R_3R3R_3 满足 BCNF 条件。
    2. 对于 R4R_4,投影到 R4R_4 的 FD 为:{CE}\{C \to E\}CC 不是 RR 的码,违反 BCNF 条件。R4R_4 不属于 BCNF,需要继续分解。
  3. 分解 R4=(A,C,E)R_4 = (A,C,E):选择 {CE}\{C \to E\},分解为 R5=(CE)=(C,E)R_5 = (C \cup E) = (C, E)R6=R4(EC)=(A,C)R_6 = R_4 - (E - C) = (A, C).
    1. 对于 R5R_5,投影到 R5R_5 的 FD 为:CEC \to EC+={C,E}=R5C⁺ = \{C, E\} = R_5R5R_5 满足 BCNF 条件。
    2. 对于 R6R_6,投影到 R6R_6 的 FD 无非平凡依赖,满足 BCNF 条件。

至此,RR 被分解为 R1,R2,R3,R4,R5,R6R_1, R_2, R_3, R_4, R_5, R_6 6 个属于BCNF 的关系模式,它们的分解过程图如下:

范式例题和总结#

范式例题#

对于范式化这一章节,主要考点是判断当前关系模式是否为 3NF 或 BCNF,判断其是否能被无损连接分解,或能否被保持依赖分解,以及递归分解为BCNF 的算法。

在此之前,我们需要找出当前关系模式的所有函数依赖及其候选码,在上一章的例题中一题提到,可以跳转查看:函数依赖例题和总结

  1. 如果有这样的关系模式 R=(Customer, Store, Product, Price)R = (\text{Customer, Store, Product, Price}) 有如下的限制:
  • 一个用户只会从一个商店购买商品。
  • 一个商店中的一个商品只能有一个价格。

请你找到蕴含的所有函数依赖关系和候选码,并判断当前关系模式是否为 3NF 或 BCNF。

运用函数依赖的知识,我们可以分析:

  • 一个用户只会从一个商店购买商品:一个用户对应一个商店,一个商店可以有多个用户。 CustomerStore\text{Customer} \to \text{Store}
  • 一个商店中的商品只能有一个价格:一个商店中的一个商品只能有一个价格,价格由商店和商品决定。 Store, ProductPrice\text{Store, Product} \to \text{Price}

所以,F={CustomerStore,{Store, Product}Price}F = \{ \text{Customer} \to \text{Store}, \{\text{Store, Product}\} \to \text{Price} \}

我们列出 FF 相关的四类属性:

  • L: {Customer}\{\text{Customer}\}
  • R: {Price}\{\text{Price}\}
  • N: \emptyset
  • LR: {Store}, {Product}\text{\{Store\}, \{Product\}}

所以,{Customer}\{\text{Customer}\} 是部分码。

  • 加入 {Store}\{\text{Store}\} 后,得到 {Customer, Store}\{\text{Customer, Store}\}{Customer, Store}+={Customer, Store}R\{\text{Customer, Store}\}^+ = \{\text{Customer, Store}\} \neq R,所以它不是候选码。
  • 加入 {Product}\{\text{Product}\} 后,得到 {Customer, Product}\{\text{Customer, Product}\},计算 ${\text{Customer, Product}}^+:
    • CustomerStore\text{Customer} \to \text{Store}: 加入 {Store}\{\text{Store}\} 得到 {Customer, Product, Store}\{\text{Customer, Product, Store}\}
    • Store, ProductPrice\text{Store, Product} \to \text{Price}: 加入 {Price}\text{\{Price\}} 获得 {Customer, Product, Store, Price}=R\{\text{Customer, Product, Store, Price}\} = R

所以 {Customer, Product}\{\text{Customer, Product}\} 是唯一的候选码。

接下来我们判断当前关系模式是否为 3NF 或 BCNF。

  • 对于 CustomerStore\text{Customer} \to \text{Store},不存在平凡依赖,Customer\text{Customer} 不是 RR 的超码,但是码的一部分。
  • 对于 {Store, Product}Price\{\text{Store, Product}\} \to \text{Price},不存在平凡依赖,Store, Product\text{Store, Product} 不是 RR 的超码,也不是 RR 的码的一部分。

所以,当前关系模式既不属于 3NF,也不属于 BCNF。

  1. R=(A,B,C,D,E),F={ABC,CD}R = (A, B, C, D, E), F = \{A → BC, C → D\},它分解为 R1=(A,B,C),R2=(A,D,E)R_1 = (A,B,C), R_2 = (A,D,E)。请你判断:

  • 该分解是否为 无损连接分解
  • 该分解是否为 保持依赖分解
  • 该分解是否为 BCNF 分解
  • 是否可以 BCNF 分解 的情况下保持依赖关系?

对于这样的关系模式,我们先要分析其候选码:

  • L: {A}\{A\}
  • R: {B},{D}\{B\},\{D\}
  • N: {E}\{E\}
  • LR: {C}\{C\}

于是,{A,E}\{A, E\} 是部分码。测试 {A,E}+\{A,E\}^+

{A,E}+={ABCDE}=R\{A,E\}^+ = \{ABCDE\} = R

所以,{AE}\{AE\} 是唯一候选码。

观察 FF,可以发现其中蕴藏着隐藏的函数依赖关系:

AC,CD    ADA \to C, C \to D \implies A \to D

  1. 对于无损连接分解,我们测试 R1R2=(A,B,C,D,E)=RR_1 \Join R_2 = (A,B,C,D,E) = R并且要求 R1R2=AR_1 \cap R_2 = AAA 至少是 R1R_1R2R_2 的超码

    1. 我们验证 R1R_1:它投影到的 FD: {ABC}\{A → BC\},很明显 AAR1R_1 的候选码。
    2. 对于 R2R_2:它投影到的 FD: {AD}\{A \to D\},很明显 EE 一定在候补码中,AA 不是 R2R_2 的候选码。

    当前分解满足:公共属性 AAR1R_1 的超码,所以该分解为无损连接分解。

  2. 对于保持依赖分解,F1={ABC}F_1 = \{A → BC\}F2={AD}F_2 = \{A \to D\}(F1F2)+={ABC,AD}+F+(F_1 \cup F_2)^+ = \{A → BC, A \to D\}^+ \neq F^+,所以该分解不是保持依赖分解。

对于当前的关系模式,我们先判断其是否为 BCNF:

  • ABCA \to BC: AA 不是 RR 的超码,违反 BCNF 条件。
  • CDC \to D: CC 不是 RR 的超码,违反 BCNF 条件。

所以,RR 可以进行 BCNF 分解。

对于 ABCA \to BC: 分解为 R3=(A,B,C)R_{3} = (A,B,C)R4=(A,D,E)R_{4} = (A,D,E) 我们发现 R3=R1R_{3} = R_1, R4=R2R_{4} = R_2

  • 投影到 R3R_{3} 的 FD 为:ABCA \to BC,此时候选码是 AA
    • ABCA \to BCAAR3R_3 的码,符合BCNF 条件。
  • 投影到 R4R_{4} 的 FD 为:ADA \to D,此时候选码是 AEAEAA 不是其候补码,不满足 BCNF 条件,需要继续分解。

所以,R1R_1R2R_2 的分解 不是 BCNF 分解。

对于最后一个问题,是否可以找到 BCNF 分解,并保持依赖关系?我们根据第二问的判断,发现是缺失了依赖 CDC \to DCCR1(A,B,C)R_1(A,B,C) 中,DDR2(A,D,E)R_2(A,D,E) 中。所以我们可以尝试进行其他的 BCNF 分解。

我们在前面的 BCNF 分解中,先按照 ABCA \to BC 分解。我们尝试使用 CDC \to D 分解:

分解为 R5=(C,D)R_5 = (C, D)R6=(A,C,B,E)R_6 = (A,C,B,E)

  • 投影到 R5R_5 的 FD 为:CDC \to D,此时候选码是 CC。此时满足 BCNF 条件。
  • 投影到 R6R_6 的 FD 为:ABCA \to BC,候选码是 AEAE
    • ABCA \to BCAA 不是 RR 的超码,违反 BCNF 条件,需要继续分解。

继续分解 R6R_6,分解为 R7=(A,B,C)R_7 = (A,B,C)R8=(A,E)R_8 = (A,E)

  • 投影到 R7R_7 的 FD 为:ABCA \to BC,此时候选码是 AA。此时满足 BCNF 条件。
  • 投影到 R8R_8 的 FD 为:\emptyset,此时满足 BCNF 条件。

所以,该路线的最终 BCNF 分解图为:

R
/ \
CD ACBE
/ \
ABC AE

RR 可以分解为 R5=(C,D),R7=(A,B,C),R8=(A,E)R_5 = (C, D), R_7 = (A,B,C), R_8 = (A,E)

此时我们继续检查是否满足保持依赖关系:

  • F5={CD}F_5 = \{C \to D\}
  • F7={ABC}F_7 = \{A \to BC\}
  • F8=F_8 = \emptyset

(F5F7F8)+=({CD}{ABC})+=F+(F_5 \cup F_7 \cup F_8)^+ = (\{C \to D\} \cup \{A \to BC\} \cup \emptyset)^+ = F^+

所以,即满足 BCNF 分解,并保持依赖关系的分解为:R5=(C,D),R7=(A,B,C),R8=(A,E)R_5 = (C, D), R_7 = (A,B,C), R_8 = (A,E)

范式总结#

让我来总结一下:

  1. 对于 3NF 的判断方法:
Step 1: 找出所有候选码
Step 2: 逐个检查每个FD
1. X是超码? → 满足3NF
2. A是部分码? → 满足3NF
3. FD 为平凡依赖? → 满足3NF
4. 都不满足? → 违反3NF
Step 3: 所有FD都满足 → 关系模式是3NF
  1. 对于 BCNF 的判断方法:
Step 1: 找出所有候选键
Step 2: 对每个FD: X → Y
1. X 是超码? → 该FD满足BCNF
2. FD 为平凡依赖? → FD满足BCNF
3. 都不满足 → 违反BCNF
Step 3: 所有FD都满足 → 关系模式是BCNF
  1. 无损连接分解判断:可以使根据定义 R1R2=RR_1 \Join R_2 = R 且满足自然连接属性 AA 是候选码的一部分。或可以根据推论:(R1R2)R1(R1 ∩ R2) → R1(R1R2)R2(R1 ∩ R2) → R2F+F⁺ 中,即 公共属性能决定其中一个关系的所有属性
  2. 保持依赖分解判断:可以直接检查 (F1F2)+(F_1 \cup F_2)^+ 是否等于 F+F^+
  3. BCNF 分解算法 - 递归分解:找到违反BCNF的依赖,分解为两个子关系,递归处理。分解结果一定满足BCNF,分解一定是无损连接,最终结果唯一性不保证(取决于选择顺序)。在选择违反BCNF的依赖时,优先选择右边属性少的(减少分裂),优先选择闭包小的(保留更多原始结构)。

事务#

事务及其特性#

事务 (Transaction) 是访问或修改数据库的一个程序执行单元,它要么全部执行,要么全部不执行。

在 SQL 语句中,使用 BEGIN TRANSACTION 开始一个事务,COMMIT 提交事务,ROLLBACK 回滚事务,END TRANSACTION 结束事务。

数据库事务具有四个主要特点,通常称为 ACID特性

  1. 原子性(Atomicity):事务中的所有操作 要么全部成功,要么全部失败,不会出现部分成功、部分失败的情况。
    • 示例:在银行转账操作中,如果从一个账户扣款成功,但未能成功存入另一个账户,整个转账操作将被回滚,确保资金不丢失。
  2. 一致性(Consistency)事务执行前后,数据库都处于一致 的状态,即数据库的完整性约束没有被破坏。
    • 示例:在银行转账操作中,事务开始前后,所有账户的总余额应保持不变。
  3. 隔离性(Isolation):一个事务在未提交之前,对其他事务是不可见的,多个事务并发执行时,彼此之间不会互相影响
    • 示例:在并发环境下,一个事务读取的数据不会受到其他未提交事务的影响。
  4. 持久性(Durability):一旦事务提交,其所做的修改将 永久保存 在数据库中,即使系统崩溃也不会丢失。
    • 示例:在银行转账操作中,一旦事务提交,转账结果将被永久记录,即使系统发生故障,转账结果也不会丢失。

事务在数据库中有四种基本操作(用同名变量和数据项表示):

  • read_item(X): 将名为 X 的数据项读入内存,存为 X
  • write_item(X): 将内存中的数据项 X 写入数据库中的数据项 X
  • commit:提交事务,将所有事务操作写入数据库。
  • abort:放弃事务,撤销所有事务操作。

当事务 TT 访问数据库的所有操作都已成功执行,并且所有事务操作对数据库的影响都已记录在日志 (Log) 中时,事务 TT 就达到了 提交点 (Commit Point)。

事务会在日志中写入一条记录 [commit, T],并且其会永久影响数据库中的记录。

不过,如果没有通过某个检查或者事务在其活动状态期间被中止,那么事务也可能进入失败状态 (failed state)。然后需要进行回滚操作 (Rollback) 以撤销其 WRITE 操作对数据库的影响。

下面是事务执行时的状态转换图:

调度#

调度 (Schedule): 是指一组事件的操作顺序。S={T1,T2,...,Tn}S = \{T_1, T_2, ..., T_n\}。来自不同事务的操作可以在调度 SS 中交替执行。

例如下面两个调度,我们使用 r,w,c,ar,w,c,a 分别表示读、写、提交和放弃事务。

  • S1S_1: r1(X);r2(X);w1(X);r1(Y);w2(X);w1(Y);r_1(X); r_2(X); w_1(X); r_1(Y); w_2(X); w_1(Y);
  • S2S_2: r1(X);w1(X);r2(X);w2(X);r1(Y);a1;r_1(X); w_1(X); r_2(X); w_2(X); r_1(Y); a_1;

对于一个调度中的两个操作,它可能会出现冲突的情况。在调度中,来自不同事务的两个操作如果满足以下条件,则称为 冲突操作(Conflicting Operations):来自不同事务的两个操作同时作用于同一数据项 (如 X),且至少有一个是写操作

常见的冲突情况有如下三种:

  1. 写-读冲突 (WR Conflict):事务 TiTᵢ 写了数据项 X,事务 TjTⱼ 读了 X。此时事务 TjTⱼ 读取了 TiTᵢ 未提交的数据,称为 脏读(Dirty Read)。
  2. 读-写冲突 (RW Conflict): 事务 TiTᵢ 读了数据项 X,事务 TjTⱼ 写了 X。此时事务 TiTᵢ 读了 TjTⱼ 未提交 的修改后的数据,如果之前也读过一次,则会出现数据不一致的问题,称为 不可重复读 (Non-Repeatable Read)。
  3. 写-写冲突 (WW Conflict): 事务 TiTᵢ 写了数据项 X,事务 TjTⱼ 也写了 X,此时事务 TjTⱼ 重写了 TiTᵢ 未提交的数据,称为 丢失更新(Lost Update)/ 脏写(Dirty Write)。
冲突类型操作顺序名称问题
读-写冲突 (WR)TiTᵢXTjTⱼX脏读/不可重复读TiTᵢ 可能读到不一致数据
写-读冲突 (RW)TiTᵢXTjTⱼX脏读TjTⱼ 可能读到未提交数据
写-写冲突 (WW)TiTᵢXTjTⱼX丢失更新/脏写一个写操作被覆盖

可恢复调度#

对于某些调度,很容易从事务和系统失败中恢复,而对于另外一些调度,恢复过程可能相当复杂。如果按照 是否可恢复 (Recoverable) 来分类不同的调度:

  • 不可恢复调度 (Non-recoverable Schedule): 如果事务 T2 读取了 T1 写入的数据 (T2 存在脏读),但 T2 在 T1 提交前就提交了,这就是不可恢复调度。
  • 可恢复调度 (Recoverable Schedule): 如果 T2 读取了 T1 写入的数据,则 T1 必须在 T2 提交前提交。这样的调度称为可恢复调度。
  • 级联回滚调度 (Cascading Rollback Schedule): 一个事务读取了另一个未提交事务写的数据。如果写事务回滚,读事务也必须回滚。
  • 无级联调度 (Cascadeless Schedule): 一个调度中的事务 只能读取已提交事务写入的数据
  • 严格调度 (Strict Schedule): 事务对数据项的读写操作都 必须等到前面对该数据项写入的事务提交或回滚后 才能进行。

例如:

  • 不可恢复调度:如果T1后来回滚了,T2已经提交了,无法撤销!数据库进入不一致状态。
T1: r1(x), w1(x), ───────────, abort1
T2: r2(x), commit2
↑ T2读了T1的脏数据并提交
↑ T1回滚(无法挽回)
  • 可恢复调度:如果T1回滚,T2还没提交,可以一起回滚。
T1: r1(x), w1(x), ─────, commit1
T2: r2(x), ─────────, commit2
↑ T2读了T1写的数据
↑ T1先提交
↑ T2后提交
  • 级联回滚调度:一个事务失败,引发多个事务回滚。T2读了T1写的 x(未提交),T3读了T2写的 y(未提交),T1回滚 → T2必须回滚 → T3必须回滚。
T1: r1(x), w1(x), ────────────────, abort1
T2: r2(x), w2(y), ─────, abort2 (被迫放弃)
T3: r3(y), ───, abort3 (被迫放弃)
  • 无级联调度:一个事务回滚不会影响其他事务。即使T2回滚,T1不受影响。
T1: r1(x), w1(x), commit1, ────────
T2: r2(x), w2(x), commit2
↑ T2只读已提交的数据
  • 严格调度:任何读和写操作都必须在事务提交或回滚后进行。
T1: r1(x), w1(x), commit1, ────────
T2: r2(x), w2(x), commit2
↑ 所有操作都在T1提交后

严格调度和无级联调度的区别是:严格调度要求 读和写操作 都必须在事务提交或回滚后进行,而无级联调度则 允许写操作 在其他事务提交或回滚之前进行。

无级联允许:
r1(x), w1(x), w2(x), commit1, commit2 ← T2的写可以在T1提交前
严格调度不允许:
r1(x), w1(x), commit1, w2(x), commit2 ← T2的写必须等T1提交

例如如下的调度 S1,S2,S3S_1, S_2, S_3,如何确定它们是严格调度、无级联调度、级联回滚调度、可恢复调度还是不可恢复调度?

  • S1S_1: r1(X);w1(X);r2(X);r1(Y);w2(X);c2;c1;r_1 (X); w_1 (X); r_2 (X); r_1 (Y); w_2 (X); c_2; c_1;
  • S2S_2: r1(X);w1(X);r2(X);r1(Y);w2(X);w1(Y);c1;c2;r_1 (X); w_1 (X); r_2 (X); r_1 (Y); w_2 (X); w_1 (Y); c_1; c_2;
  • S3S_3: r1(X);w1(X);w2(X);w1(Y);c1;r2(Y);c2;r_1 (X); w_1 (X); w_2 (X); w_1 (Y); c_1; r_2 (Y); c2;

我们判断这些调度时,可以遵循如下步骤:

  1. 识别 依赖关系:找出哪些操作存在读-写依赖,即 读或写同一个变量,但是在不同的事务中
  2. 检查 提交顺序:判断是否可恢复
  3. 检查 读未提交:判断是否级联/无级联
  4. 检查 写未提交:判断是否严格

我们识别这些调度的标号,发现只有两个事务,所以我们将其改写为时间轴。

S1S_1 时间轴:

T1: r₁(X) → w₁(X) → ─────── r₁(Y) → ────────── c₁
T2: r₂(X) → ────────── w₂(X) → c₂
T2读了T1写的X(未提交)

S2S_2 时间轴:

T1: r₁(X) → w₁(X) → ─────── r₁(Y) → ────── w₁(Y) → c₁
T2: r₂(X) → ───────────── w₂(X) → ────── c₂
T2读了T1写的X(未提交)

S3S_3 时间轴:

T1: r₁(X) → w₁(X) → ─────── w₁(Y) → c₁
T2: w₂(X) → ──────────────── r₂(Y) → c₂
T2读了T1写的Y(已提交)
  1. S1S_1: 可以发现 r2(X)r₂(X) 依赖 w1(X)w₁(X)(T2读了T1写的 X),T2 读取了 T1 写的未提交脏数据。在调度的最后,T2 先于 T1 提交,如果 T1 回滚了,T2 无法回滚,这是 不可恢复调度
  2. S2S_2: 可以发现 r2(X)r₂(X) 依赖 w1(X)w₁(X)(T2读了T1写的 X),T2 读取了 T1 写的未提交脏数据。在调度的最后,T1 先于 T2 提交,如果 T1 回滚了,T2 也必须回滚,这是 级联回滚调度
  3. S3S_3: 可以发现 r2(Y)r₂(Y) 依赖 w1(Y)w₁(Y)(T2读了T1写的 Y),但是 T1 已提交。还可以发现一个 WW 关系:w2(X)w₂(X) 发生在 w1(X)w₁(X) 之后,此时 T2 写入了 T1 未提交的数据,这是 无级联调度

所以:S1S_1不可恢复调度S2S_2级联回滚调度S3S_3无级联调度

如果想要把 S3S_3 改为 严格调度,只需要将 w2(X)w₂(X) 移到 c1c₁ 之后即可。如下 S4S_4 是由 S3S_3 修改而来的严格调度:

S4S_4: r1(X);w1(X);w1(Y);c1;w2(X);r2(Y);c2;r₁(X); w₁(X); w₁(Y); c₁; w₂(X); r₂(Y); c₂;

T1: r₁(X) → w₁(X) → w₁(Y) → c₁
T2: w₂(X) → r₂(Y) → c₂
↑ 等T1提交后再写

可串行调度#

在实际情况中,我们经常会并发地运行多个事务,即进行 串行调度 (Serial Schedule): 所有事务在事务集合中按一个确定的顺序依次进行,没有交叉。

例如: T1 → T2(完全串行)

T1: r₁(X); w₁(X); r₁(Y); w₁(Y); c₁;
T2: r₂(X); w₂(X); r₂(Z); w₂(Z); c₂;

但是,如果有多个事务,这样的调度效率低下。于是,我们引入 可串行化调度 (Serializable Schedule),即:允许并发执行,但结果必须等价于某个串行调度的调度。它保证了充分利用并发,提高吞吐量,且 保证结果与串行执行一致

相对的,我们将与任何串行调度都不等价的调度称为 不可串行化调度 (Non-Serializable Schedule)。

我们通过 结果等价 (Result Equivalence) 和 冲突等价 (Conflict Equivalence) 来定义可串行化的等价。

  • 结果等价 (Result Equivalence): 两个调度对于相同的初始数据库状态,产生相同的最终结果

例如:初始状态 X = 10

调度 S1S₁(串行):

T1: r₁(X); X=10
w₁(X=20); ← 写X=20
c₁;
T2: r₂(X); X=20
w₂(X=30); ← 写X=30
c₂;
最终结果:X = 30

调度 S2S₂(可串行化调度):

T1: r₁(X); X=10
T2: r₂(X); X=10
T1: w₁(X=20); ← T1写X=20
T2: w₂(X=30); ← T2写X=30(覆盖)
T1: c₁;
T2: c₂;
最终结果:X = 30

结果等价的结果相同,但 S2S₂ 中 T2 读的是初始值(10),S1S₁ 中 T2 读的是 T1 写的值(20),它们中间状态不同,只是最终结果 碰巧相同

  • 冲突等价 (Conflict Equivalence): 两个调度满足以下条件则冲突等价:包含相同事务的相同操作,且 每对冲突操作的顺序相同

我们回顾冲突操作的顺序:

冲突类型示例顺序重要吗?
WRw₁(X) → r₂(X)✅ 是
RWr₁(X) → w₂(X)✅ 是
WWw₁(X) → w₂(X)✅ 是
RRr₁(X) → r₂(X)❌ 否

例如:

  1. 调度 S1S₁r1(A);w1(A);r2(A);r2(B);w2(B);r1(B);w1(B);r₁(A); w₁(A); r₂(A); r₂(B); w₂(B); r₁(B); w₁(B);
  2. 调度 S2S₂r1(A);w1(A);r1(B);w1(B);r2(A);r2(B);w2(B);r₁(A); w₁(A); r₁(B); w₁(B); r₂(A); r₂(B); w₂(B);

我们可以分析其冲突对:

  1. S1S₁ 冲突对:
    1. w1(A)r2(A)w₁(A) → r₂(A):WR冲突
    2. w1(B)w2(B)w₁(B) → w₂(B):WW冲突
    3. r2(B)w1(B)r₂(B) → w₁(B):RW冲突
  2. S2S₂ 冲突对:
    1. w1(A)r2(A)w₁(A) → r₂(A):WR冲突
    2. w1(B)w2(B)w₁(B) → w₂(B):WW冲突
    3. r1(B)w2(B)r₁(B) → w₂(B):RW冲突

我们可以发现 S1S_1S2S_2 在第三对冲突对中的顺序不一样,所以认为 S1S_1S2S_2 不是冲突等价

有了冲突等价,我们可以得到一种基于冲突等价的可串行化调度:如果一个调度 SS冲突可串行化 (Conflict Serializability) 的,当且仅当它与某个串行调度 冲突等价

例如,

  1. S3S_3w1(A);r2(B);r1(B);r2(A);w₁(A); r₂(B); r₁(B); r₂(A);
  2. S4S_4w1(A);r1(B);r2(B);r2(A);w₁(A); r₁(B); r₂(B); r₂(A);

我们分析其冲突对:

  1. S3S_3 冲突对:w1(A)r2(A)w₁(A) → r₂(A):WR冲突
  2. S4S_4 冲突对:w1(A)r2(A)w₁(A) → r₂(A):WR冲突

此时,S3S_3S4S_4 是冲突等价的,因为 S4S_4 是串行调度,所以 S3S_3 是冲突可串行化的。

判断调度是否是冲突可串行化时,我们一般使用 优先图 (Precedence Graph) 测试法。

我们有如下定义:一个优先图 G=(V,E)G = (V, E) 是一个有向图,其 顶点集为事务集合边集为事务之间的冲突关系

  • 节点 (V):每个事务一个节点
  • (E):如果存在冲突操作 opi(X)opj(X)opᵢ(X) → opⱼ(X),则画边 TiTjTᵢ → Tⱼ

若调度 SS 的优先图 GG无环 的 (Acyclic),则 SS冲突可串行化 (Conflict Serializable) 的。

构造优先图的步骤很简单:

  1. 列出所有事务 (V):根据下标,列出所有参与调度的事务。
  2. 列出所有冲突关系 (E):根据冲突对,列出所有冲突关系。
  3. 根据 步骤 1 和 2 构造优先图 GG,检查优先图 GG 是否无环。
  4. 如果无环,则调度 SS 是冲突可串行化的。如果存在环,则调度 SS 不是冲突可串行化的。

下面是一些例子:

对于:S3S_3w1(A);r2(B);r1(B);r2(A);w₁(A); r₂(B); r₁(B); r₂(A);,画出其优先图。

  1. 列出所有事务:T1,T2T_1, T_2
  2. 列出所有冲突关系:w1(A)r2(A)w₁(A) → r₂(A)
  3. 构造优先图 G=(V,E)G = (V, E)T1T2T_1 → T_2,它显然没有环,所以 S3S_3 是冲突可串行化的。

对于:S5S_5w1(a);r3(a);r1(b);w2(b);w3(c);r4(c);w2(d);r4(d);w_1(a); r_3(a); r_1(b); w_2(b); w_3(c); r_4(c); w_2(d); r_4(d);,画出其优先图。

  1. 列出所有事务:T1,T2,T3,T4T1, T2, T3, T4
  2. 列出所有冲突关系:注意只找同一个数据的。
    1. WR: w1(a);r3(a);w_1(a); \to r_3(a); T1T3T1 \to T3
    2. RW: r1(b);w2(b);r_1(b); \to w_2(b); T1T2T1 \to T2
    3. WR: w3(c);r4(c);w_3(c); \to r_4(c); T3T4T3 \to T4
    4. WR: w2(d);r4(d);w_2(d); \to r_4(d); T2T4T2 \to T4
  3. 构造优先图 G=(V,E)G = (V, E)
G:
T1
/ \
T2 T3
\ /
T4
  1. 检查优先图 GG 是否无环。很明显,存在一个环 T1T3T4T2T1T1 \to T3 \to T4 \to T2 \to T1T1T2T4T3T1T1 \to T2 \to T4 \to T3 \to T1,所以 S5S_5 不是冲突可串行化的。

事务例题和总结#

事务例题#

事务这一章主要要求我们识别和判断不同的调度类型。

按照可恢复调度分类有:不可恢复调度、可恢复调度、级联回滚调度、无级联调度和严格调度。

按可串行化分类有:不可串行化、冲突可串行化。

  1. 有这样的调度 S=w2(b);r1(a);w1(a);r2(a);c1;c2;S = w_2(b); r_1(a); w_1(a); r_2(a); c1; c2;. 请你判断该调度在按照可恢复调度分类和可串行化分类下,分别属于哪类调度。

对于这样的问题,我们依然找出其冲突对:w1(a);r2(a);w_1(a); \to r_2(a);,这是一个 WR 冲突。

按可串行化分类,如果 SS可串行化 的,则 SS 的优先图 GG无环 的。

构造优先图 G=(V,E)G = (V, E)T1T2T1 \to T2,很明显无环。所以 SS冲突可串行化 的。

按可恢复调度分类,我们发现:发生这个冲突会导致:T2 读取 T1 未提交的数据,如果 T1 回滚,T2 也必须回滚,所以这是一个 级联回滚调度。注意:因为 T1 先于 T2 提交,所以 SS 依然是 可恢复 的,只是不是 无级联回滚 的。

如果想要将 SS 改为无级联回滚的,则需要解决冲突对 w1(a);r2(a);w_1(a); \to r_2(a);。所以一个简单的解决方案是在 T2 读之前先提交 T1。

S2S_2: w2(b);r1(a);w1(a);c1;r2(a);c2;w_2(b); r_1(a); w_1(a); c1; r_2(a); c2;

  1. 对于一个调度 S=r3(Z);w3(Z);r1(X);r2(Y);w2(Y);w1(X);r1(Y);r3(X);c1;c2;c3;S = r_3(Z); w_3(Z); r_1(X); r_2(Y); w_2(Y); w_1(X); r_1(Y); r_3(X); c1; c2; c3;,判断它是否是可串行化的,是否可恢复,是否是无级联回滚的。

同样,我们分析其冲突对,因为这个调度较长,我们可以将其改写为时间轴的方式容易发现冲突对:

T1: r1(X) → w1(X) → r1(Y) → c1
T2: r2(Y) → w2(Y) → c2
T3: r3(Z) → w3(Z) → r3(X) → c3

可以发现:

  1. w1(X);r3(X);w_1(X); \to r_3(X);: WR 冲突
  2. w2(Y);r1(Y);w_2(Y); \to r_1(Y);: WR 冲突

对于可串行化,我们可以构造优先图 G=(V,E)G = (V, E):节点有 T1,T2,T3T1, T2, T3,边有 T1T3,T2T1T1 \to T3, T2 \to T1

T1 → T3
T2

构造优先图 GG 无环,所以 SS 是冲突可串行化的。

对于可恢复调度,要求如果 TjT_j 读取了 TiT_i 写入的数据,则 TiT_i 必须在 TjT_j 提交前提交。在该题中则要求提交顺序为:T2,T1,T3T2, T1, T3

因为 SS 的提交顺序为 T1,T2,T3T1, T2, T3,所以 SS不可恢复 的。

此外,因为 T3 读取了 T1 未提交的数据,T1 读取了 T2 未提交的数据,当 T2 回滚时,T1和T3 也必须回滚。所以 SS级联回滚 的。

若要把 SS 改为可恢复的调度,则需要调整提交顺序。S2=r3(Z);w3(Z);r1(X);r2(Y);w2(Y);w1(X);r1(Y);r3(X);c2;c1;c3;S_2 = r_3(Z); w_3(Z); r_1(X); r_2(Y); w_2(Y); w_1(X); r_1(Y); r_3(X); c2; c1; c3;

若要把 SS 改为无级联回滚的调度,则需要在读取前提交。S3=r3(Z);w3(Z);r1(X);r2(Y);w2(Y);c2;w1(X);r1(Y);c1;r3(X);c3;S_3 = r_3(Z); w_3(Z); r_1(X); r_2(Y); w_2(Y); c2; w_1(X); r_1(Y); c1; r_3(X); c3;

事务总结#

总结一下事务的例题和常见考法:

首先最重要的是:找出冲突对

冲突类型模式优先图边含义
WRwᵢ(X) → rⱼ(X)Tᵢ → Tⱼj读了i写的值
RWrᵢ(X) → wⱼ(X)Tᵢ → Tⱼj覆盖了i读的值
WWwᵢ(X) → wⱼ(X)Tᵢ → Tⱼj覆盖了i写的值

冲突对找出后,就可以构造优先图 G=(V,E)G = (V, E),并检查 GG 是否无环。

GG 无环,则 SS冲突可串行化 的。

对于可恢复性的判断:根据冲突对去检查提交顺序。因为其要求读取其他事务写的数据前提交,它决定的是最后提交的顺序,没有要求一定要读取前提交。所以只需要找 读了谁,谁先提交 即可判断。

对于无级联回滚的判断:在可恢复的基础上,更加强硬的要求另一个事务的提交顺序必须在读取之前。所以只需要找 读之前,必提交 即可判断。

而对于最为严格的严格调度,在无级联回滚的基础上,还要求不能出现同写一个数据块,且未提交的操作。所以需要找 写之后,必提交 即可判断。

这些调度的关系如下:严格调度 ⊂ 无级联回滚 ⊂ 可恢复 ⊂ 可串行化 ⊂ 所有调度

┌─────────────────────────────────┐
│ Step 0: 预处理 │
│ - 给操作编号 │
│ - 识别事务和数据项 │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Step 1: 判断可串行化 │
│ ├─ 找冲突对 │
│ ├─ 画优先图 │
│ ├─ 检测环 │
│ └─ 输出结果 │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Step 2: 判断可恢复 │
│ ├─ 找读依赖 │
│ ├─ 检查提交顺序 │
│ └─ 输出结果 │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Step 3: 判断无级联 │
│ ├─ 检查每个读操作 │
│ ├─ 判断写者是否已提交 │
│ └─ 输出结果 │
└─────────────────────────────────┘
【数据库】基础回顾专题
https://hoyue.fun/database_final.html
作者
Hoyue
发布于
2025-10-31
许可协议
CC BY-NC-SA 4.0
评论