依赖

关系数据库设计

略去前置知识,保留一些比较重要的知识,不保证连贯性,需要自行阅读书本后再来进行串联起来


关系模式的分解

在数据库设计中,分解(Decomposition)是指将一个关系模式拆分为多个子模式,以减少冗余、消除数据异常,并提高数据一致性。


1. 无损分解(Lossless Decomposition)

如果关系模式 RR 被分解成 R1和 R2,且通过 R1 和 R2的自然连接(Natural Join)能够无损地恢复原始关系 RR,则称该分解为无损分解(Lossless Decomposition)。

数学表达:

  • R(A,B,C)R(A, B, C) 被分解为 **R1(A,B)**和 **R2(B,C)**。
  • 如果对任意可能的数据集合,执行自然连接R1⋈R2= R 且不会产生额外的元组,也不会丢失信息,则该分解是无损的

2. 有损分解(Lossy Decomposition)

如果关系模式的分解导致信息丢失或产生额外的错误数据,则称为有损分解(Lossy Decomposition)。

示例:有损分解

考虑以下关系:

1
employee(ID, name, street, city, salary)

我们尝试将其分解为:

1
2
employee1(ID, name)
employee2(name, street, city, salary)

问题分析

  • 这个分解的交集属性集是 {name},但 name 并不是主键,因为可能存在重名的员工

  • 当我们进行自然连接:

    1
    SELECT * FROM employee1 NATURAL JOIN employee2;

    可能会出现原关系中不存在的错误数据(即笛卡尔积导致的额外元组)。

示例数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
employee1:
+----+------+
| ID | name |
+----+------+
| 1 | 张三 |
| 2 | 李四 |
+----+------+

employee2:
+------+---------+------+--------+
| name | street | city | salary |
+------+---------+------+--------+
| 张三 | A街道 | X市 | 5000 |
| 李四 | B街道 | Y市 | 6000 |
| 张三 | C街道 | Z市 | 7000 | -- 另一个张三
+------+---------+------+--------+
  • 由于 name不是唯一的,当 employee1employee2进行自然连接时:

    1
    SELECT * FROM employee1 NATURAL JOIN employee2;

    可能会产生错误的组合,如:

    1
    2
    (1, 张三, A街道, X市, 5000)
    (1, 张三, C街道, Z市, 7000) -- 这个组合可能不正确!

    这表示 ID=1的张三可能会匹配到错误的地址,导致数据不一致。

结论

  • 因为连接后产生了原关系中不存在的元组,该分解是有损的!

3. 如何确保无损分解?

一个分解 R→R1,R2 是无损的,当且仅当:

  • R1∩R2(两个子关系的交集)至少包含一个候选码(Candidate Key)的属性

无损分解示例

假设 employee 关系:

1
employee(ID, name, street, city, salary)

我们进行如下无损分解

1
2
employee1(ID, name, street, city)
employee2(ID, salary)
  • 这里 ID 是主键,且 ID 作为交集出现在 employee1employee2 中。

  • 由于 ID 可以唯一标识一个员工,所以自然连接:

    1
    SELECT * FROM employee1 NATURAL JOIN employee2;

    可以正确恢复 employee无丢失也无多余元组


4. 小结

分解类型 特点
无损分解 通过自然连接可以完全恢复原始数据,不会丢失信息,也不会产生额外的元组。
有损分解 自然连接后可能会丢失信息或产生错误的额外元组,导致数据不一致。

确保无损分解的方法:交集属性 至少包含候选码的一部分,这样才能确保自然连接时不会丢失数据。


函数依赖

定义

关系数据库中,函数依赖(Functional Dependency, FD)描述的是属性之间的关系,表示一个属性的值能够唯一确定另一个属性的值

数学定义:

设 R(U) 是一个关系模式,X 和 Y 是 U 的属性集,如果对于 R 中的任意两个元组 t1 和 t2,若 t1[X] = t2[X],则必有 t1[Y] = t2[Y],则称 X 函数确定 Y,记作:

X→Y

读作 “X 函数确定 Y” 或 “Y 依赖于 X”

简单来说,如果知道 X 的值,就可以唯一确定 Y 的值

用简单的话来解释下这个定义:假设我们现在有一个模式

1
department(dept_name,building,budget)

当我们查询任意一个元组是,如果我们已经知道了其中的一个dept_name,而且我们能通过这个dept_name区推导得出一个唯一的building值的话,那我们称这俩个属性在这个模式中构成函数依赖。而且是由dept_name决定building,所以应该表达为dept_name→building。

在E-R模型的学习中。我们应该理解到了,无论是E-R模型,还是关系数据库模型,其本质都只是一个模型。而模型的本质则是对于现实世界的一种抽象,所以很多概念都是共通的,只是一些定义上的些许差别。我们需要的是一种从已有的网状知识图谱中去找到一条能通往新学习的知识的路线。而不是从0开始再进行知识点网的构建。

所以所谓函数依赖其实可以视为是E-R模型中的映射基数中的一对一情况在关系模式中的体现。

有了函数依赖这个概念,我们回过头来看到我们之前碰到的关于超码的分析

​ 我们先来重温下超码的逻辑概念。

在一个模式中,当元组的一个或多个属性的集合能够唯一的标识出一个元组时,这个属性的集合可以视为该模式的一个超码。

​ 对于超码来说,函数依赖的逻辑定位与其十分相像,对于一个函数依赖,我们能够通过一个或多个属性来推导出另外的一个或多个属性。不难联想到,当我们函数依赖的推导方与被推导方的并集能够完美的覆盖整个模式时,这种函数依赖的推导方应该能被称之为超码。

​ 对于一个函数依赖来推导超码的情况,有一点需要特别注意。当我们使用一个逻辑上的试图来推导函数依赖时,此时我们判断的基础是当前数据库模式的一个快照。

​ 就比如,我们拥有如下一个模式

1
classroom(building,room_number,capacity)

​ 我们根据该模式的一个快照来进行函数依赖的推导,此时所有的room_number都有着其一个capacity,而且所有的capacity都不相同。此时我们可能会把room_number->capacity作为一个函数依赖来进行使用。但是,这样是否存在问题呢?

​ 我们抽象回来看到现实视图。在一个教学楼中可能存在着各种容量的教室,这没有问题。在多个教学楼中,存在着多个编号相同的教室,这也没有问题。在不同教学楼的编号相同的教室中,出现相同容量的情况,这也没有问题吧。但是问题就来了,这里如果我们的逻辑层函数依赖room_number->capacity成立,那么在之后某次可能的SQL插入中,完全有可能插入一个除了building之外完全相同的元组,此时我们的函数依赖room_number->capacity就不成立了。

​ 基于上面那个例子,我想强调的是一句话:

一个关系的实例可能满足的某些函数依赖并不需要再该关系的模式上成立。

简单来说,我们在一个数据库模式中去思考函数依赖的构建,其都是基于当前关系的一个快照推导的。此时快照推导出来的函数依赖不存在问题不代表着该模式在接下来的使用中都不会存在问题。因此,我们得看到一个模式的本质,其是对现实世界中存在的事物的抽象建模,我们对于函数依赖的思考应该是基于这层抽象建模而不应该基于当前系统的一个快照。

​ 因此,我们可以总结出来自己的一句话

一个模式的快照实例可能满足的某些函数依赖,并不意味着这些依赖在该关系的模式上永远成立。

下面给出一个测试demo,自行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- 1. 创建表
CREATE TABLE classroom (
building VARCHAR(10),
room_number INT,
capacity INT
);

-- 2. 插入数据,当前快照满足 "room_number → capacity"
-- 对于 room_number 101,总是对应 capacity 为 50
INSERT INTO classroom (building, room_number, capacity) VALUES ('A', 101, 50);
INSERT INTO classroom (building, room_number, capacity) VALUES ('B', 102, 60);
INSERT INTO classroom (building, room_number, capacity) VALUES ('C', 101, 50);

-- 查询当前数据
SELECT * FROM classroom;
-- 结果:
-- | building | room_number | capacity |
-- |----------|-------------|----------|
-- | A | 101 | 50 |
-- | B | 102 | 60 |
-- | C | 101 | 50 |

-- 3. 插入一个新记录,打破 "room_number → capacity" 的依赖
-- 由于 room_number 101 在前面均对应 50,现在插入一个 capacity 为 100 的记录
INSERT INTO classroom (building, room_number, capacity) VALUES ('D', 101, 100);

-- 再次查询数据
SELECT * FROM classroom;
-- 结果:
-- | building | room_number | capacity |
-- |----------|-------------|----------|
-- | A | 101 | 50 |
-- | B | 102 | 60 |
-- | C | 101 | 50 |
-- | D | 101 | 100 |

-------------本文结束 感谢阅读-------------