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 中可以使用集合运算符对关系进行运算,例如 交 INTERSECT, 并 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) 投影 (project) 重命名 (rename)
  2. 集合关系 (set operation): 集合交 (Intersection) 集合并 (Union) 集合差 (Difference) 笛卡尔积 (Cartesian product) / 叉乘 (Cross product)
  3. 二元关系 (binary operation): 连接 (join) 等值连接 (equi-join) 自然连接 (natural join) 除运算 (Division)

一元关系#

  1. 选择 (selection) : 用于选择一个满足条件的元组。 可以嵌套即
  2. 投影 (projection) : 用于选择一列属性。 会进行去重。
  3. 重命名 (rename) : 用于给关系 重命名 为 。还可以为关系的属性进行重命名: 表格中的属性 重命名为

集合关系#

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

  1. 并集 (union) ,表示 的并集。
  2. 交集 (intersection) ,表示 的交集。
  3. 差集 (difference) ,表示 的差集。.
  4. 叉乘 (cross product) ,表示 的叉乘,包含 的所有属性和值,形成的新关系表格的属性顺序从左 () 到右 ()。

例如:

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) ,表示 的连接,包含 的所有属性和值,形成新的关系表格。
  2. 等值连接 (equal join) ,表示 根据对应字段相等的连接,不包含对应属性的列,包含 的其他列,形成新的关系表格。
  3. 自然连接 (natural join) ,表示 根据所有同名字段相等的连接,不包含所有同名属性的列,包含 的其他列,形成新的关系表格。

例如:

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

对于等值连接 ,结果为:

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

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

  1. 除法 (Devision) 找出 中那些与 中所有 Y 值都有关联的 X 值。 中的每个元素,都必须和 中的每一个元素配对出现在 中。

例如下面的学生选课问题 的例子, 的结果为:

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

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

除法适用于处理 “全部”、“所有”、“每一个”相关的查询需求。它在 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*)

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

SELECT Ename
FROM Employee E
WHERE E.eid = 5;

这是一个减法,即先选取 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
);

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

SELECT E.Ename
FROM Employee E
WHERE NOT EXISTS (
SELECT *
FROM Works W
WHERE W.eid = E.eid
);

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

SELECT Employee.Ename, Department.eid
FROM Employee E, Works W, Department D
WHERE Employee.eid = Works.eid AND Works.did = Department.did;

我们从内向外拆解:

  • 是在选择满足条件的行,然后进行笛卡尔积,最后投影出 E1.eid 列。它是从笛卡尔积中选择 E1 的薪水小于 E2 的薪水的行。
  • 是从 E1 中删除 I 中的行,然后投影出 E1.eid 列。即找出 没有人薪水比自己高的员工ID(即薪水最高的员工)
  • 是进行自然连接,保留所有具有相同属性名及其值的行。即获取薪水最高的员工的 完整记录
  • 最后投影出 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):

这一题内层是一个等值连接,当属性 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. 找到预订所有的船的船员名字

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

  • 所有船:
SELECT B.bid
FROM Boat B;
  • 所有被预订的船,因为最后要找到船员名字,所以需要留下 sid 去匹配船员信息。我们获取预订关系的(船员ID, 船ID)对:
SELECT R.sid, R.bid
FROM Reservation R
WHERE R.bid = B.bid AND R.sid = S.sid;
  • 执行除法,找出预订了所有船的船员ID:
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表连接并投影,并获取船员名字:
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很相似,除数从”所有船”变为”所有红船”,增加了一层条件。所以此时的除数变成是”所有红船”。

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

  • 所有红船:
SELECT B.bid
FROM Boat B
WHERE B.color = 'red';
  • 所有预订关系的(船员ID, 船ID)对:
SELECT R.sid, R.bid
FROM Reservation R
WHERE R.bid = B.bid AND R.sid = S.sid;
  • 执行除法,找出预订了所有船的船员ID:
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表连接并投影,并获取船员名字:
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关键语法转换说明
选择WHERE根据条件筛选行,直接将关系代数的条件表达式放入WHERE子句
投影SELECT A, B, C选择指定列,将下标中的属性列表放入SELECT子句
UNION合并两个查询结果,自动去重;需保留重复则用UNION ALL
EXCEPT返回在R中但不在S中的元组;部分数据库用MINUS
INTERSECT返回同时存在于R和S中的元组
笛卡尔积CROSS JOIN,两表的所有组合,FROM子句用逗号或CROSS JOIN
θ-连接JOIN ... ON θ笛卡尔积+条件筛选,θ条件放入ON子句或WHERE子句
等值连接JOIN ... ON A = Bθ-连接的特例,连接条件为等值比较
自然连接NATURAL JOIN自动匹配同名属性并去重列;或手动指定公共属性等值连接
除法NOT EXISTS 嵌套
GROUP BY ... HAVING COUNT
双重否定法:不存在S中某元组使得R中不存在匹配
计数法:按R的非公共属性分组,计数等于S的元组数

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

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

函数依赖#

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

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

Definition

关系模式 的两个属性子集 之间的函数依赖记作 ,表示 属性的值由 属性决定。

若在 上取值相同的元组在 上也相等,对于 中的任意两个元组 ,如果有 ,则 ,则说明 FD 成立, 决定

例如:

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 |

我们可以发现 中存在 FD ,因为当 时,。注意,其中存在有 这样的元组,因为这个 对应关系和下标无关

但是,FD 不代表 ,因为存在 这样的对应关系,所以函数依赖反过来不一定成立。

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

此时若有:

  • FD ,则该 FD 称为 平凡函数依赖 (Trivial FD)。例如,,这样的FD 称为平凡函数依赖。
  • FD ,则该 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 是一个平凡的函数依赖,因为 sname{sid, sname} 的一个子集。而 FD 则是一个非平凡的函数依赖。不能说 是平凡的,因为这个没有实际意义,非函数依赖。

超码和候补码#

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

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

例如:

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

此时存在这样的函数依赖:, ,

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

我们把一个关系的所有函数依赖的集合称为关系 R函数依赖集 (Function Dependency set),记为 。例如:

如果说一个函数依赖 ,且能被 中的函数依赖所推导出来,则称 蕴含 (Imply)。例如, 蕴含,因为 表示 能确定 可以确定 ,故 可以确定

闭包#

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

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

  1. 自反律 (Reflexivity): 如果 ,则 。例如
  2. 增广律 (Augmentation): 如果 ,则 (对任意属性集 )。例如:,则
  3. 传递律 (Transitivity): 如果 ,则 。例如:,则

还有三个派生规则:

  1. 合并律 (Union): 如果 ,则
  2. 分解律 (Decomposition): 如果 ,则
  3. 部分传递律 (Partial Transitivity): 如果 ,则

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

例如,

, ,求 的闭包

  1. 我们先添加 属性自身:
  2. 开始逐步运用三大定律和已有的

最后得到结果:

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

例如:

,求属性闭包

  1. 初始化自身属性:
  2. 遍历每个函数依赖若属性未被加入,则加入该属性并计算其闭包
    • 对于 不在闭包中,则加入
    • 计算 : ,则加入
    • 计算 ,则加入

最后,我们得到了

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

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

计算 :

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

计算

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

根据函数依赖的定义: 满足函数依赖关系,当且仅当 中的属性值 对于任意元组 满足 时,

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

  1. 其中,Title 是电影的名字,Theater 是电影院的名字,City 是电影院所在的城市。

现在有如下限制:

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

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

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

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

那我们可以推导出:

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

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

  • :

    • 对于 Theater → City,
    • 对于 City, Title → Theater,

    所以

  • :

    • 对于 Theater → City,所以
    • 对于 City, Title → Theater,

    所以 .

  • :

    • 对于 Theater → City,
    • 对于 City, Title → Theater,

    所以 .

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

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

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

先找到四种属性分类:

  • L (只在 FD 左侧出现):
  • R (只在 FD 右侧出现):
  • N (不在 FD 中出现):
  • LR (既在 FD 左侧又在 FD 右侧出现):

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

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

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

解析这些限制关系:

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

所以我们可以推导出:

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

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

  • :

    • A → C: ,所以 .
    • A → B: 同上,
    • {C, B} → A: ,没有属性需要加入。

    所以 .

  • :

    • A → C:
    • A → B:
    • {C, B} → A:

    所以 .

  • :

    • A → C:
    • A → B:
    • {C, B} → A:

    所以 .

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

可以发现,,它的两个子属性闭包不等于 ,所以 也是一个候选码。

  1. 我们不知道它的码是什么,如果 是一个候选码,你会怎么样去检测它?使用 SQL 语句去检测。

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

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

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

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

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

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

所以 .

  1. :
    • X → Y: , 没有属性需要加入。
    • {U, V} → W: , 没有属性需要加入。
    • V → X: , 没有属性需要加入。

所以 .

  1. :
    • X → Y: , 没有属性需要加入。
    • {U, V} → W: , 没有属性需要加入。
    • V → X: , 没有属性需要加入。

所以 .

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

所以 .

  1. :
    • X → Y: , 没有属性需要加入。
    • {U, V} → W: , 没有属性需要加入。
    • V → X: , 没有属性需要加入。

所以 .

函数依赖总结#

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

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

概念定义示例
平凡依赖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)两边都出现可能在候选码中

对于关系 , 函数依赖集 :

  1. Step 1: 属性分类:计算 L, R, N, LR 类属性
  2. Step 2: 初始候选码 (这些属性必须在候选码中)
  3. Step 3: 检验 Base: 计算 ,如果 ,则 Base 是唯一候选码。否则添加 LR 属性,对 LR 类属性的每个子集 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}

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

有一个数据库:, ,求

  1. 先取自反:
  2. 检查 : 发现 属于当前闭包,加入后续属性到闭包中:
  3. 可以直接检查 ,因为 属于当前闭包:
  4. 检查 ,因为此时 均在闭包中,需要重新检查其他函数依赖:
  5. 检查 ,无元素可以添加:

结果: 是候选码。


范式化#

范式#

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

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

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

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

  • 部分依赖 (Partical Dependency):一个函数依赖关系形如:,若存在子集 (),使得 ,则称 部分依赖
  • 传递依赖 (Transitive Dependency):函数依赖关系形如:,则称 传递依赖

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

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

  • BCNF: 若 属于 BCNF,对于任意 ,满足:
    1. 平凡依赖,或
    2. 超码
  • 3NF: 若 属于 3NF,对于任意 ,满足:
    1. 平凡依赖,或
    2. 超码,或
    3. 某个候选键的一部分

例如:

,判断是否属于 BCNF。

  • 对于 的码;
  • 对于 ,不存在平凡依赖 () 且 不为码,所以 不属于 BCNF。

,判断是否属于 3NF。

  • 对于 的码;
  • 对于 ,不存在平凡依赖 (), 不为码, 是码 的一部分。

所以 属于 3NF。

分解#

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

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

  1. 无损连接分解 (Lossless Join Decomposition): 将一个关系模式经过分解后通过自然连接能恢复原关系。即,。根据这个条件,我们还可以推导出:两个分解关系的公共属性 () 必须是其中 至少一个关系 () 的超码。这意味着:
  2. 保持依赖分解 (Dependency Preserving Decomposition): 将一个关系模式分解为多个关系模式,使得这些关系模式满足依赖关系。即,

例如:

,分解为 ,请判断是否为无损连接分解。若分解为 ,请判断是否为无损连接分解。

对于 ,我们计算 ,所以它是一个无损连接分解。

对于 ,我们计算 ,但是自然连接的条件 并非 的候选码,连接的数据可能不一致,所以它不是一个无损连接分解。

,若 。分解为 ,请判断是否为保持依赖分解。若分解为 ,请判断是否为保持依赖分解。

对于 , ,所以它不是一个保持依赖分解。

对于 , ,它是一个保持依赖分解。

对于 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

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

,请判断 是否为 BCNF,若不是,请分解为 BCNF。

我们先判断其是否为 BCNF:

  • 对于 不是 的码,违反 BCNF 条件。
  • 对于 不是 的码,违反 BCNF 条件。
  • 对于 不是 的码,违反 BCNF 条件。

所以 不属于 BCNF。

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

  1. 分解 :选择 ,分解为 . 接下来,递归分解
    1. 对于 ,投影到 的 FD 为: 满足 BCNF 条件。
    2. 对于 ,投影到 的 FD 为:,对于 不是 的码 ,违反 BCNF 条件;对于 不是 的码,违反 BCNF 条件。 不属于 BCNF,需要继续分解。
  2. 分解 :选择 ,分解为 .
    1. 对于 ,投影到 的 FD 为: 满足 BCNF 条件。
    2. 对于 ,投影到 的 FD 为: 不是 的码,违反 BCNF 条件。 不属于 BCNF,需要继续分解。
  3. 分解 :选择 ,分解为 .
    1. 对于 ,投影到 的 FD 为: 满足 BCNF 条件。
    2. 对于 ,投影到 的 FD 无非平凡依赖,满足 BCNF 条件。

至此, 被分解为 6 个属于BCNF 的关系模式,它们的分解过程图如下:

范式例题和总结#

范式例题#

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

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

  1. 如果有这样的关系模式 有如下的限制:
  • 一个用户只会从一个商店购买商品。
  • 一个商店中的一个商品只能有一个价格。

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

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

  • 一个用户只会从一个商店购买商品:一个用户对应一个商店,一个商店可以有多个用户。
  • 一个商店中的商品只能有一个价格:一个商店中的一个商品只能有一个价格,价格由商店和商品决定。

所以,

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

  • L:
  • R:
  • N:
  • LR:

所以, 是部分码。

  • 加入 后,得到 ,所以它不是候选码。
  • 加入 后,得到 ,计算 ${\text{Customer, Product}}^+:
    • : 加入 得到
    • : 加入 获得

所以 是唯一的候选码。

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

  • 对于 ,不存在平凡依赖, 不是 的超码,但是码的一部分。
  • 对于 ,不存在平凡依赖, 不是 的超码,也不是 的码的一部分。

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

  1. ,它分解为 。请你判断:

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

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

  • L:
  • R:
  • N:
  • LR:

于是, 是部分码。测试

所以, 是唯一候选码。

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

  1. 对于无损连接分解,我们测试 并且要求 至少是 的超码

    1. 我们验证 :它投影到的 FD: ,很明显 的候选码。
    2. 对于 :它投影到的 FD: ,很明显 一定在候补码中, 不是 的候选码。

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

  2. 对于保持依赖分解,,所以该分解不是保持依赖分解。

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

  • : 不是 的超码,违反 BCNF 条件。
  • : 不是 的超码,违反 BCNF 条件。

所以, 可以进行 BCNF 分解。

对于 : 分解为 我们发现 ,

  • 投影到 的 FD 为:,此时候选码是
    • 的码,符合BCNF 条件。
  • 投影到 的 FD 为:,此时候选码是 不是其候补码,不满足 BCNF 条件,需要继续分解。

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

对于最后一个问题,是否可以找到 BCNF 分解,并保持依赖关系?我们根据第二问的判断,发现是缺失了依赖 中, 中。所以我们可以尝试进行其他的 BCNF 分解。

我们在前面的 BCNF 分解中,先按照 分解。我们尝试使用 分解:

分解为

  • 投影到 的 FD 为:,此时候选码是 。此时满足 BCNF 条件。
  • 投影到 的 FD 为:,候选码是
    • 不是 的超码,违反 BCNF 条件,需要继续分解。

继续分解 ,分解为

  • 投影到 的 FD 为:,此时候选码是 。此时满足 BCNF 条件。
  • 投影到 的 FD 为:,此时满足 BCNF 条件。

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

R
/ \
CD ACBE
/ \
ABC AE

可以分解为

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

所以,即满足 BCNF 分解,并保持依赖关系的分解为:

范式总结#

让我来总结一下:

  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. 无损连接分解判断:可以使根据定义 且满足自然连接属性 是候选码的一部分。或可以根据推论: 中,即 公共属性能决定其中一个关系的所有属性
  2. 保持依赖分解判断:可以直接检查 是否等于
  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:放弃事务,撤销所有事务操作。

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

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

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

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

调度#

调度 (Schedule): 是指一组事件的操作顺序。。来自不同事务的操作可以在调度 中交替执行。

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

  • :
  • :

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

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

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

可恢复调度#

对于某些调度,很容易从事务和系统失败中恢复,而对于另外一些调度,恢复过程可能相当复杂。如果按照 是否可恢复 (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提交

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

  • :
  • :
  • :

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

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

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

时间轴:

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

时间轴:

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

时间轴:

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

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

如果想要把 改为 严格调度,只需要将 移到 之后即可。如下 是由 修改而来的严格调度:

:

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

调度 (串行):

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

调度 (可串行化调度):

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

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

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

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

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

例如:

  1. 调度
  2. 调度

我们可以分析其冲突对:

  1. 冲突对:
    1. :WR冲突
    2. :WW冲突
    3. :RW冲突
  2. 冲突对:
    1. :WR冲突
    2. :WW冲突
    3. :RW冲突

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

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

例如,

我们分析其冲突对:

  1. 冲突对::WR冲突
  2. 冲突对::WR冲突

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

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

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

  • 节点 (V):每个事务一个节点
  • (E):如果存在冲突操作 ,则画边

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

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

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

下面是一些例子:

对于:,画出其优先图。

  1. 列出所有事务:
  2. 列出所有冲突关系:
  3. 构造优先图 ,它显然没有环,所以 是冲突可串行化的。

对于:,画出其优先图。

  1. 列出所有事务:
  2. 列出所有冲突关系:注意只找同一个数据的。
    1. WR:
    2. RW:
    3. WR:
    4. WR:
  3. 构造优先图
G:
T1
/ \
T2 T3
\ /
T4
  1. 检查优先图 是否无环。很明显,存在一个环 ,所以 不是冲突可串行化的。

事务例题和总结#

事务例题#

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

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

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

  1. 有这样的调度 . 请你判断该调度在按照可恢复调度分类和可串行化分类下,分别属于哪类调度。

对于这样的问题,我们依然找出其冲突对:,这是一个 WR 冲突。

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

构造优先图 ,很明显无环。所以 冲突可串行化 的。

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

如果想要将 改为无级联回滚的,则需要解决冲突对 。所以一个简单的解决方案是在 T2 读之前先提交 T1。

:

  1. 对于一个调度 ,判断它是否是可串行化的,是否可恢复,是否是无级联回滚的。

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

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

可以发现:

  1. : WR 冲突
  2. : WR 冲突

对于可串行化,我们可以构造优先图 :节点有 ,边有

T1 → T3
T2

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

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

因为 的提交顺序为 ,所以 不可恢复 的。

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

若要把 改为可恢复的调度,则需要调整提交顺序。

若要把 改为无级联回滚的调度,则需要在读取前提交。

事务总结#

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

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

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

冲突对找出后,就可以构造优先图 ,并检查 是否无环。

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

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

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

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

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

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