关系数据库设计
约 4893 个字 预计阅读时间 33 分钟
引言
关系数据库的良好设计对于数据一致性、消除冗余和提高性能至关重要。本章介绍关系数据库设计的理论基础,包括函数依赖、范式和关系分解算法等核心概念。通过这些理论和方法,我们可以系统地评估关系模式的质量,并且在需要时进行分解以改进设计。
函数依赖
-
函数依赖基本概念
函数依赖是关系数据库中最基本的完整性约束之一,它定义了属性之间的确定关系:
- 函数依赖是对合法关系集合的约束
- 要求某一组属性的值能唯一确定另一组属性的值
- 函数依赖是键概念的推广
- 允许我们找到左侧不是超键的依赖关系
函数依赖的定义
定义
设R是一个关系模式,X和Y是R的属性子集。如果对于R的任何合法实例r,r中任意两个元组t₁和t₂,当它们在X上的值相同时,它们在Y上的值也相同,则称函数依赖X→Y在R上成立。
形式化表示为:t₁[X] = t₂[X] ⟹ t₁[Y] = t₂[Y]
例如:考虑关系模式inst_dept(ID, name, salary, dept_name, building, budget),以下函数依赖可能成立:
- ID → name, salary, dept_name (ID是超键)
- dept_name → building, budget (dept_name不是超键)
而以下函数依赖可能不成立:
- dept_name → salary (不同系的教师可能有不同的薪水)
函数依赖与键的关系
-
超键与候选键
- 超键(Superkey):如果X→R,则X是关系模式R的超键
- 候选键(Candidate Key):如果X→R且对于X的任何真子集Y,Y→R不成立,则X是R的候选键
函数依赖是确定键的基础,通过分析函数依赖可以找出关系模式的所有候选键。
平凡与非平凡函数依赖
函数依赖X→Y可分为以下类型:
- 平凡函数依赖:如果Y⊆X,则X→Y是平凡的(所有关系实例都满足)
-
例如:ID, name → ID 或 name → name
-
非平凡函数依赖:如果Y⊈X,则X→Y是非平凡的
对于非平凡函数依赖X→Y,我们关注两个方面:
- X是否为超键
- Y是否包含候选键的一部分
函数依赖的闭包
-
函数依赖闭包
给定一组函数依赖F,由F逻辑蕴含的所有函数依赖构成F的闭包,记为F⁺。
例如:如果A→B且B→C,则可以推断出A→C(通过传递性)。
F⁺是F的超集,包含F以及所有从F推导出的函数依赖。
Armstrong公理
计算函数依赖闭包可以使用Armstrong公理:
- 自反性(Reflexivity):如果Y⊆X,则X→Y
- 增广律(Augmentation):如果X→Y,则XZ→YZ(对于任意属性集Z)
- 传递性(Transitivity):如果X→Y且Y→Z,则X→Z
这些规则是:
- 健全的(Sound):只生成实际成立的函数依赖
- 完备的(Complete):生成所有成立的函数依赖
额外的推导规则:
- 合并规则:如果X→Y且X→Z,则X→YZ
- 分解规则:如果X→YZ,则X→Y且X→Z
- 伪传递规则:如果X→Y且WY→Z,则WX→Z
计算函数依赖闭包的算法
函数依赖闭包算法:
F⁺ = F
repeat
for each 函数依赖 f in F⁺
将自反性和增广律应用于f
将结果添加到F⁺
for each 函数依赖对 f₁和f₂ in F⁺
如果f₁和f₂可以通过传递性组合
then 将结果添加到F⁺
until F⁺不再变化
属性闭包
-
属性闭包
给定属性集X和函数依赖集F,X在F下的闭包(记为X⁺)是X在F下函数确定的所有属性的集合。
属性闭包是一个强大的工具,可用于:
- 测试超键:如果X⁺包含R的所有属性,则X是R的超键
- 测试函数依赖:要检查X→Y是否在F⁺中,只需检查Y是否包含在X⁺中
- 计算F的闭包
计算属性闭包的算法
计算属性闭包X⁺的算法:
result := X;
while (result有变化) do
for each 函数依赖Y→Z in F do
if Y ⊆ result then result := result ∪ Z;
例子:
假设R = (A, B, C, G, H, I),F = {A→B, A→C, CG→H, CG→I, B→H}
计算(AG)⁺:
- result = AG
- result = ABCG (A→C和A→B)
- result = ABCGH (CG→H且CG⊆ABCG)
- result = ABCGHI (CG→I且CG⊆ABCGH)
因此(AG)⁺ = ABCGHI
检查AG是否为候选键:
- 是否为超键?是,因为(AG)⁺包含R的所有属性
- 是否有AG的子集是超键?
- (A)⁺ = ABC...(不包含所有属性)
- (G)⁺ = G(不包含所有属性)
因此AG是候选键。
函数依赖的用途
函数依赖有多种实用价值:
- 指定对合法关系集合的约束
-
我们说F在R上成立,如果R上所有合法关系都满足F中的所有函数依赖
-
测试关系是否满足给定的函数依赖集
-
如果关系r满足F中的所有函数依赖,则称r满足F
-
如果R不是良好形式,将R分解为R₁...Rₙ,使每个Rᵢ都是良好形式
-
在某些情况下,将多个关系合并为一个关系
注意
关系模式的特定实例可能满足某个函数依赖,即使该函数依赖在所有合法实例上都不成立。例如,instructor的特定实例可能偶然满足name→ID。
关系分解
当关系模式不满足所需的范式时,需要进行分解。良好的分解应该具有以下性质:
- 无损连接分解(Lossless-join Decomposition):确保通过自然连接操作可以恢复原始关系中的所有信息
- 依赖保持(Dependency Preservation):确保所有原始函数依赖都能在分解后的模式中得到保持和检查
无损连接分解
-
无损连接分解
对于关系模式R分解为R₁和R₂,如果对R的任何合法实例r,都有:
r = πR₁(r) ⋈ πR₂(r)
则称该分解是无损连接的。
充分条件:如果以下依赖中至少有一个在F⁺中,则分解是无损连接的:
- R₁∩R₂ → R₁
- R₁∩R₂ → R₂
合并关系
在某些情况下,我们可能需要合并关系,特别是当多个关系共享相同的键时。
例如:
- dept_building(dept_name, building)
- dept_budget(dept_name, budget)
可以合并为:
- department(dept_name, building, budget)
在这种情况下,合并不会导致冗余。
规范化与范式
第一范式
-
第一范式(1NF)
如果关系模式R的所有属性的域都是原子的(不可分割的单位),则称R满足第一范式(1NF)。
原子性实际上是域使用方式的属性,而不是域本身的内在属性。
非原子域的例子:
- 名称集合、复合属性
- 可以分解的标识符(如CS101)
非原子值会使存储复杂化并鼓励数据冗余存储。
Boyce-Codd范式(BCNF)
-
BCNF定义
如果关系模式R对函数依赖集F满足下列条件,则称R满足BCNF:对于F⁺中的任何非平凡函数依赖X→Y,X必须是R的超键。
换句话说,对于每个非平凡函数依赖X→Y,如果Y⊈X,则X必须是超键。
BCNF保证了没有冗余和插入/删除/更新异常。
BCNF的例子
考虑模式:instr_dept(ID, name, salary, dept_name, building, budget)
函数依赖:
- ID → name, salary, dept_name
- dept_name → building, budget
这个模式不满足BCNF,因为dept_name不是超键,但dept_name → building, budget成立。
BCNF分解
如果关系模式R因为非平凡函数依赖X→Y违反BCNF,我们可以将R分解为:
- R₁ = (X∪Y)
- R₂ = (R - (Y-X))
对于上面的例子,instr_dept被分解为:
- department(dept_name, building, budget)
- instructor(ID, name, salary, dept_name)
在这个分解中,每个关系都满足BCNF,而且分解是无损连接的。
第三范式(3NF)
-
第三范式(3NF)
如果关系模式R对函数依赖集F满足下列条件,则称R满足3NF:对于F⁺中的任何非平凡函数依赖X→Y,至少满足以下条件之一:
- X是R的超键
- Y-X中的每个属性A都包含在R的某个候选键中
3NF是BCNF的放宽,为了确保依赖保持而设计。
如果关系满足BCNF,它也满足3NF。
3NF的例子
考虑关系模式:dept_advisor(s_ID, i_ID, dept_name) 函数依赖:
- s_ID, dept_name → i_ID
- i_ID → dept_name
候选键:{s_ID, dept_name}和{i_ID, s_ID}
这个关系满足3NF,因为:
- 对于s_ID, dept_name → i_ID,左侧是候选键
- 对于i_ID → dept_name,dept_name包含在候选键{s_ID, dept_name}中
3NF的冗余
虽然3NF允许一些冗余,但它确保了所有函数依赖都可以在不连接关系的情况下进行检查。这是3NF相对于BCNF的主要优势。
例如,在关系R = (J, K, L)上,如果F = {JK→L, L→K},由于依赖保持的原因,我们可能更倾向于保持这个3NF关系而不进行分解。
函数依赖理论与算法
规范覆盖
函数依赖集中可能存在冗余的依赖,这些依赖可以从其他依赖推导出来。规范覆盖(Canonical Cover)是一种找出最小函数依赖集的方法。
-
规范覆盖概念
函数依赖集F的规范覆盖Fc是满足以下条件的函数依赖集:
- F逻辑蕴含Fc中的所有依赖
- Fc逻辑蕴含F中的所有依赖
- Fc中没有包含多余属性的函数依赖
- Fc中每个函数依赖的左侧都是唯一的
规范覆盖移除了冗余信息,是数据库设计和优化的重要工具。
多余属性
在函数依赖X→Y中,如果某个属性是多余的,则可以移除它而不影响所表示的约束。
- 左侧多余属性:如果A∈X且F逻辑蕴含(X-A)→Y,则A在X中是多余的
- 右侧多余属性:如果A∈Y且F' = (F - {X→Y}) ∪ {X→(Y-A)}逻辑蕴含X→Y,则A在Y中是多余的
计算规范覆盖的算法
计算F的规范覆盖的算法:
Fc = F
repeat
使用合并规则替换Fc中任何依赖X→Y1和X→Y2为X→Y1Y2
找出Fc中带有多余属性的函数依赖X→Y
如果找到多余属性,则从X→Y中删除它
until Fc不再变化
例子:
R = (A, B, C),F = {A→BC, B→C, A→B, AB→C}
- 合并A→BC和A→B为A→BC
- 集合变为{A→BC, B→C, AB→C}
- A在AB→C中是多余的
- 检查删除A后的依赖是否被其他依赖蕴含
- 是的:B→C已经存在!
- 集合变为{A→BC, B→C}
- C在A→BC中是多余的
- 检查A→C是否被A→B和其他依赖逻辑蕴含
- 是的:使用A→B和B→C的传递性
- 最终规范覆盖为{A→B, B→C}
3NF分解算法
为了将关系分解为3NF,同时保持无损连接和依赖保持,可以使用以下算法:
3NF分解算法:
1. 计算F的规范覆盖Fc
2. 对Fc中的每个函数依赖X→Y:
如果没有已创建的模式包含X∪Y
则创建一个包含X∪Y的新模式
3. 如果没有模式包含R的候选键,则额外创建一个包含某个候选键的模式
4. 删除任何被其他模式包含的模式
该算法确保:
- 每个生成的关系模式都满足3NF
- 分解是无损连接的
- 分解是依赖保持的
例子:
考虑关系模式:cust_banker_branch = (customer_id, employee_id, branch_name, type)
函数依赖:
- customer_id, employee_id → branch_name, type
- employee_id → branch_name
- customer_id, branch_name → employee_id
计算规范覆盖:
- branch_name在第1个依赖的右侧是多余的
- 得到Fc = { customer_id, employee_id → type, employee_id → branch_name, customer_id, branch_name → employee_id }
应用3NF分解算法:
- 创建(customer_id, employee_id, type)
- 创建(employee_id, branch_name)
- 创建(customer_id, branch_name, employee_id)
- (customer_id, employee_id, type)包含候选键,不需要额外创建
- (employee_id, branch_name)是(customer_id, branch_name, employee_id)的子集,可以删除
最终3NF模式:
- (customer_id, employee_id, type)
- (customer_id, branch_name, employee_id)
BCNF分解算法
BCNF分解算法类似于3NF分解,但不保证依赖保持:
BCNF分解算法:
result := {R};
done := false;
compute F+;
while (not done) do
如果result中存在不满足BCNF的模式Ri
then
找出一个在Ri上成立的非平凡函数依赖X→Y,其中X不是Ri的超键
result := (result - {Ri}) ∪ {(Ri - Y), (X ∪ Y)};
else
done := true;
例子:
考虑关系模式:class(course_id, title, dept_name, credits, sec_id, semester, year, building, room_number, capacity, time_slot_id)
函数依赖:
- course_id → title, dept_name, credits
- building, room_number → capacity
- course_id, sec_id, semester, year → building, room_number, time_slot_id
候选键:{course_id, sec_id, semester, year}
应用BCNF分解:
- course_id → title, dept_name, credits成立
- course_id不是class的超键
-
分解为:
- course(course_id, title, dept_name, credits)
- class-1(course_id, sec_id, semester, year, building, room_number, capacity, time_slot_id)
-
building, room_number → capacity在class-1上成立
- {building, room_number}不是class-1的超键
- 进一步分解为:
- classroom(building, room_number, capacity)
- section(course_id, sec_id, semester, year, building, room_number, time_slot_id)
最终BCNF关系:
- course(course_id, title, dept_name, credits)
- classroom(building, room_number, capacity)
- section(course_id, sec_id, semester, year, building, room_number, time_slot_id)
BCNF和3NF的比较
-
BCNF与3NF的比较
BCNF: - 消除所有冗余和异常 - 不总是能保持依赖 - 可能导致某些函数依赖只能通过连接才能检查
3NF: - 允许一些冗余 - 总是能保持依赖 - 可以在不连接的情况下检查所有函数依赖
选择依据: - 如果依赖保持很重要,选择3NF - 如果消除冗余更重要,选择BCNF
多值依赖与第四范式
多值依赖
即使关系满足BCNF,它可能仍然存在某种形式的冗余。考虑关系:
inst_info(ID, child_name, phone),其中一个教师可能有多个孩子和多个电话号码。
这个关系没有非平凡的函数依赖,因此它满足BCNF。但是,如果我们要为ID=99999添加一个新电话981-992-3443,需要为每个孩子添加一个元组,导致冗余。
-
多值依赖定义
设R是一个关系模式,X和Y是R的属性子集。多值依赖X→→Y在R上成立,当且仅当对于任意两个元组t₁和t₂,如果t₁[X] = t₂[X],则存在元组t₃和t₄满足:
- t₁[X] = t₂[X] = t₃[X] = t₄[X]
- t₃[Y] = t₁[Y]
- t₃[R-X-Y] = t₂[R-X-Y]
- t₄[Y] = t₂[Y]
- t₄[R-X-Y] = t₁[R-X-Y]
简单来说,如果X→→Y,则给定X值,Y的值集合与R-X-Y的值集合是独立的。
如果X→Y,则X→→Y(每个函数依赖也是多值依赖)。
在我们的例子中:
- ID →→ child_name(给定教师ID,孩子名称的集合与电话号码集合无关)
- ID →→ phone(给定教师ID,电话号码的集合与孩子名称集合无关)
第四范式(4NF)
-
第四范式(4NF)
关系模式R对函数依赖和多值依赖集D满足4NF,如果对于D⁺中的每个非平凡多值依赖X→→Y,X是R的超键。
如果一个关系满足4NF,它也满足BCNF。
4NF消除了BCNF中仍可能存在的冗余。
4NF分解算法
4NF分解算法:
result := {R};
done := false;
compute D+;
while (not done) do
如果result中存在不满足4NF的模式Ri
then
找出一个在Ri上成立的非平凡多值依赖X→→Y,其中X不是Ri的超键
result := (result - {Ri}) ∪ {(X ∪ Y), (X ∪ (Ri - Y))};
else
done := true;
例子:
将inst_info(ID, child_name, phone)分解为:
- inst_child(ID, child_name)
- inst_phone(ID, phone)
这两个关系都满足4NF,而且分解是无损连接的。
数据库设计过程
实体-关系模型与规范化
-
设计方法比较
自顶向下方法(E-R设计):
- 识别实体、关系和属性
- 将E-R图转换为关系模式
- 应用规范化理论验证设计
自底向上方法(规范化理论):
- 定义单一关系包含所有属性(全局关系)
- 应用规范化算法分解为更小的关系
实际上,这两种方法常常结合使用,先用E-R设计创建初步模式,再用规范化理论进行验证和调整。
从E-R模型生成的关系
当一个E-R图被仔细设计,正确识别所有实体后,从E-R图生成的表通常不需要进一步规范化。
但在实际(不完美)设计中,可能存在实体内非键属性到其他属性的函数依赖:
- 例如:一个employee实体具有department_name和building属性,且存在函数依赖department_name → building
- 好的设计应该把department做成一个实体
反规范化考虑
有时为了性能原因,可能选择使用非规范化的模式:
- 显示优点:查询性能提升,避免连接开销
-
显示缺点:
- 额外空间消耗
- 更新操作变慢
- 需要额外编码工作维护一致性
在现代数据库系统中,可以使用物化视图达到类似效果,兼顾性能和规范化的好处。
不良设计模式
规范化理论无法捕获所有不良设计,以下是应避免的一些模式:
-
属性作为列名:
-
子类型归并到单表:
时态数据考虑
时态数据具有时间关联性,在设计中需要特别考虑:
-
时态函数依赖:如果X→Y在所有时间点的快照上都成立,则称X→Y是时态函数依赖
-
实用设计方法:为关系添加开始和结束时间属性
-
外键参考:可能指向当前版本的数据,或特定时间点的数据
- 例如:学生成绩单应引用课程被选修时的课程信息
总结
关系数据库设计是一个复杂而重要的过程,涉及多种方法和理论:
- 函数依赖理论提供了评估关系质量的基础
- 规范形式(1NF, BCNF, 3NF, 4NF)定义了好的关系模式应满足的条件
- 分解算法帮助我们系统地改进关系模式
- E-R模型和规范化共同为数据库设计提供了完整的方法论
良好的数据库设计目标是:
- 尽可能达到BCNF或4NF
- 确保分解是无损连接的
- 在可能的情况下保持依赖
当这些目标发生冲突时,设计者需要在规范化程度(减少冗余)和依赖保持(维护完整性约束)之间做出权衡,基于应用需求选择合适的设计。