Skip to content
← 数据库系统

/knowledge/notes/functional-dependencies

概念笔记 · 数据库系统

函数依赖

依赖说明哪些列决定哪些列。键、闭包和范式,都来自认真对待这些承诺。

学于
数据库系统INFO20003 · COMP90050
时间
2020 年第一学期
应用于
Louvre Ops DB
阅读 / 复习
约 6 分钟阅读2026-10-21

函数依赖是对数据的一个承诺:只要两行在这几列上相同,它们在那一列上也相同。数据库设计有很大一部分工作,就是把这些承诺写下来,再安排表的结构,让每个事实只存在一个地方。这篇笔记讲一个依赖说的是什么,怎样从几个依赖推出它们蕴含的全部依赖,以及为什么无视依赖的表会让每次更新都有机会自相矛盾。

01

基本想法

当知道列 XX 的值就能确定列 YY 的值时,写作 X→YX \to Y,读作“XX 决定 YY”。在一张售票表里,票号决定票种,票种决定票价。依赖是对系统任何时候可能存的每一张表的断言,而不只是今天表里那些行。所以它是一条业务规则,来自这些列的含义。

键是能决定所有其他列、又没有多余列的一组列。声明主键,就是声明一个从键到整行的依赖。这也是为什么一个随手选的键,会悄悄地让表存不下实际发生的事情。

麻烦始于依赖的左边不是键。想象一张很宽的展馆扫描表,每一行都抄着票种和票价。成人票价就出现在每一条成人票扫描记录里。改票价时你得改所有这些行,漏掉一行,表里就同时有两个成人票价。这叫更新异常。你也没法在有人用新票种扫码之前记录它的票价,而删掉最后一条会员票扫描记录,就忘了会员票卖多少钱。规范化通过拆表一次解决这三个问题,让每个依赖的左边成为它自己那张表的键。数据库系统主题会一步步讲把这件事精确化的各个范式。

02

数学

形式上,如果一个关系里没有两行在 XX 上相同却在 YY 上不同,就说它满足 X→YX \to Y。

X→Y  ⟺  for all rows t1,t2:  t1[X]=t2[X]  ⇒  t1[Y]=t2[Y]X \to Y \iff \text{for all rows } t_1, t_2:\; t_1[X] = t_2[X] \;\Rightarrow\; t_1[Y] = t_2[Y]

一对行就足以推翻一个依赖。多少行都不足以证明一个依赖。

从一组声明的依赖 FF 出发,Armstrong 的三条规则能推出所有必然成立的依赖:自反律、增广律和传递律。

Y⊆XX→YX→YXZ→YZX→YY→ZX→Z\frac{Y \subseteq X}{X \to Y} \qquad\qquad \frac{X \to Y}{XZ \to YZ} \qquad\qquad \frac{X \to Y \quad Y \to Z}{X \to Z}

这些规则是可靠的,不会推出错误的依赖,也是完备的,能推出每一个正确的依赖。

实际上很少有人手工套这些规则,而是计算闭包:从 XX 开始,只要某个声明的依赖的左边落在当前集合里,就把它的右边加进来,直到不再变化。

X+={ A:X→A follows from F },X is a superkey  ⟺  X+=RX^{+} = \{\, A : X \to A \text{ follows from } F \,\}, \qquad X \text{ is a superkey} \iff X^{+} = R

键就是去掉任何一列都不再是超键的超键。

Boyce–Codd 范式要求每个成立的依赖左边都是超键。第三范式稍微放宽,允许右边是某个键的一部分,这样在 BCNF 做不到的时候,每个依赖仍然能在单张表里检查。

R is in BCNF  ⟺  every non-trivial X→Y that holds has X+=RR \text{ is in BCNF} \iff \text{every non-trivial } X \to Y \text{ that holds has } X^{+} = R

平凡指 Y 已经包含在 X 里,比如票号和时间决定时间。

拆表不能丢信息。把拆出来的表再连接回去,必须正好得到原来的行,不多也不少。当共享的列是其中一张表的键时,这一点就有保证。

R→R1,R2 is lossless  ⟺  (R1∩R2)→R1   or   (R1∩R2)→R2R \to R_1, R_2 \text{ is lossless} \iff (R_1 \cap R_2) \to R_1 \;\text{ or }\; (R_1 \cap R_2) \to R_2

按依赖 X 到 Y 拆分:用 X 和 Y 组成一张表,另一张表保留 X。

03

动手试

下面的工具存着一家虚构博物馆的展馆扫描日志,每次扫描一行,还有设计者声明的四条规则。用按钮拼出一个依赖,工具会分别回答两个问题:这份样本有没有违反它,它能不能从声明的规则推出来。

设计者声明的规则

  • 票号 → 票种
  • 票种 → 票价
  • 展馆 → 楼层
  • 票号, 时间 → 展馆

左边 X(选一个或多个)

16
种子 1

票号 → 展馆

在这份样本里

不成立。第 1 行和第 4 行的票号相同(T08),展馆却不同:East 对 West。

从声明的规则推出

{票号}⁺ = {票号、票种、票价}。 展馆不在闭包里,所以规则没有承诺这一点。 左边不是键。

推导:票号 → 票种,再 票种 → 票价。

按时间排序的前 16 条展馆扫描记录。X 列标 (X),Y 列标 (Y)。
#票号 (X)票种票价展馆 (Y)楼层时间
1冲突T08Member0EastL209:10
2T03Adult22EastL209:14
3T01Adult22WestL009:17
4冲突T08Member0WestL009:18
5T01Adult22EastL209:57
6T08Member0NorthL109:59
7T01Adult22WestL010:02
8T02Adult22WestL010:25
9T05Adult22WestL010:41
10T04Member0WestL010:53
11T02Adult22WestL011:08
12T04Member0EastL211:12
13T04Member0NorthL111:54
14T06Adult22EastL212:10
15T07Adult22NorthL112:38
16T06Adult22WestL012:43

不成立。第 1 行和第 4 行的票号相同(T08),展馆却不同:East 对 West。展馆不在闭包里,所以规则没有承诺这一点。

扫描记录来自一个遵守四条声明规则的带种子生成器,所以你找到的任何违反,都是没有人承诺过的依赖。
  • 默认问的是票号能不能决定展馆。数据违反了它,因为一张票会去好几个展馆,而票号的闭包只有票号、票种和票价。
  • 在左边加上时间。票号和时间一起决定展馆,闭包覆盖了所有列,这一对就是键。
  • 试试票价决定票种。16 行时它成立。把行数拖到 24,会发现一张儿童票和一张会员票都是 0 元。票种决定票价,反过来从来不是规则。
  • 试试时间决定票号。32 行以内,每次扫描都落在不同的分钟上,样本推翻不了它。到 40 行时,好几个分钟出现了重复。样本可以推翻依赖,却永远证明不了依赖。
  • 勾上拆分。有三条声明的规则左边不是键,按它们拆分后存的单元格更少,成人票价也只存在一行里。

04

我在哪用到它

05

容易出错的地方

06

参考资料