高级 #database#normalization#dependency

函数依赖

函数依赖(Functional Dependency)描述表中列之间的"决定关系"——如果学号确定,姓名就唯一确定,就说"学号 → 姓名"

🔍 “学号确定了,我是谁就确定了”

想想你的学生证:上面印着学号、姓名、学院、班级……

如果有人给你一个学号,你能根据学号查到对应学生的姓名吗?能。 因为学号和姓名是对应的——一个学号只对应一个学生。

反过来——如果有人给你一个姓名,你能根据姓名查到对应的学号吗?不一定。 因为可能有同名同姓的人,一个姓名可能对应多个学号。

这种”一个值确定另一个值”的关系,在数据库理论中被称为 函数依赖(Functional Dependency)

🧮 名字里为什么有”函数”?

回想你学过的数学函数:f(x) = y——给定输入 x,输出 y 被唯一确定。

函数依赖也是这个意思:给定学号 x,姓名 y 就被唯一确定。 写成:

学号 → 姓名(学号决定姓名)

记法:X → Y,读作”X 函数决定 Y”或”Y 函数依赖于 X”。


🎯 函数依赖的基本表达

函数依赖用 X → Y 表示:对于表中的任意两行,如果它们的 X 值相同,那么它们的 Y 值也必须相同

用实际例子理解:

学生表:
学号 → 姓名      (知道学号,就知道唯一姓名)
学号 → 年龄      (知道学号,就知道唯一年龄)
学号 → 班级      (知道学号,就知道唯一班级)
学号 → 学院      (知道学号,就知道唯一学院)

系名 → 院长      (知道系名,就知道唯一院长)
系名 → 学号      ❌(一个系有很多学生,系名不能唯一确定学号)

🏫 类比:指纹识别

每个人的指纹能唯一确定一个人。知道指纹 → 就知道姓名、年龄、住址……(指纹 → 姓名、指纹 → 年龄……)

但反过来——知道姓名不一定能确定指纹(可能有重名)。

函数依赖就是描述这种”一件事决定了另一件事”的关系。


🔑 候选键(Candidate Key)

函数依赖和”键”有直接关系。候选键(Candidate Key) 就是能唯一确定一行所有属性的最小属性集。

假设学生表有:学号, 姓名, 年龄, 班级, 身份证号

候选键有两个:
1. {学号}       → 能确定所有其他列
2. {身份证号}   → 也能确定所有其他列

主键 = 从候选键中选一个,通常是 {学号}

关键概念:候选键的”最小性”——如果你去掉候选键中的任何一个属性,它就不再能唯一确定一行了。

💡 比如 {学号, 姓名} 也能确定所有列,但它不是最小的——因为去掉”姓名”后,{学号} 已经能确定所有列了。所以 {学号, 姓名} 不是候选键。


🔄 Armstrong 公理——函数依赖的”推导规则”

函数依赖之间不是孤立的——给出一组函数依赖,你可以推导出其他的。Armstrong 公理就是推导的规则集。

三条基本规则

① 自反律(Reflexivity)

如果 Y 是 X 的子集,那么 X → Y。

例子:{学号, 姓名} → 学号
解释:{学号, 姓名} 这个组合当然能确定"学号"本身——这看起来是废话,但确保了一致性。

② 增广律(Augmentation)

如果 X → Y,那么 XZ → YZ(在两边同时加上同样的属性,依赖仍然成立)。

已知:学号 → 姓名
那么:(学号, 班级) → (姓名, 班级)
解释:如果学号确定姓名,那么学号+班级自然也确定姓名+班级。

③ 传递律(Transitivity)

如果 X → Y 且 Y → Z,那么 X → Z。

已知:学号 → 系名,系名 → 院长
那么:学号 → 院长
解释:学号决定系名,系名决定院长,所以学号间接决定了院长。

这三条规则看起来简单,但它们构成的系统是完备的——任何可以用函数依赖表达的逻辑推论,都能通过这三条规则推导出来。

一个推导示例

已知:学号 → 系名,系名 → 院长,系名 → 办公楼

由传递律:

  • 学号 → 院长(学号 → 系名,系名 → 院长)
  • 学号 → 办公楼(学号 → 系名,系名 → 办公楼)

所以学号函数决定了院长的信息——即使学号和院长之间没有直接关系。


⚠️ 为什么要关心函数依赖?

函数依赖是数据库设计质量的重要衡量标准。

不好的表设计往往包含”不合理的函数依赖”:

-- 坏设计:所有信息塞在一张表
CREATE TABLE bad_schema (
    student_id INT PRIMARY KEY,
    student_name VARCHAR(50),
    dept_name VARCHAR(50),
    dean_name VARCHAR(50),  -- 院长姓名
    course_name VARCHAR(100),
    grade INT
);

在这个表中存在三个函数依赖:

  1. student_id → student_name, dept_name(学号决定姓名和系名)
  2. dept_name → dean_name(系名决定院长)
  3. (student_id, course_name) → grade(学号+课程名决定成绩)

问题出在哪里?

  • 系名 → 院长这个依赖导致:如果某个系有 1000 个学生选了课,“院长姓名”就被重复存储了 1000 次(冗余
  • 如果院长换了,要更新所有包含该院长的行(更新异常
  • 如果一个学生还一门课都没选,他的信息甚至存不进去(插入异常
  • 如果删除所有选了某门课的学生,课程信息也可能被删掉(删除异常

如何解决这些问题?——这就是下一节 范式 的内容。


📝 小结

概念一句话
函数依赖 X → YX 的值决定 Y 的值——X 和 Y 是列名
候选键能唯一确定所有列的最小属性集
自反律子集函数依赖于父集
增广律两边同时加属性,依赖不变
传递律X → Y, Y → Z ⇒ X → Z
函数依赖的用途分析表设计质量,发现冗余和异常

🎯 思考题:如果一个表中有”身份证号 → 学号”这个函数依赖,但身份证号不是主键,这算不算好设计?为什么?

为什么先学这个? 函数依赖是衡量表设计”好坏”的数学工具。下一步用这个工具来学习范式(1NF ~ BCNF)——如何通过拆分表来消除不合理的依赖。