先看一张「什么都往里塞」的表,这是我大二数据库课设的真实作品,期末验收时被老师当场点名:
选课(学号, 课程号, 姓名, 系, 系主任, 成绩)
S001 C1 张三 CS 王大力 88
S001 C2 张三 CS 王大力 92
S002 C1 李四 CS 王大力 75
这张表能跑,但藏着三种病,每种都在某天真的咬了我一口:
根子是表里混进了好几个主题(选课、学生、系),一份数据(系主任)被复印了 N 份存在 N 行里。本章给出诊断工具(函数依赖)和处方(范式):按依赖把表拆到每个主题一份,冗余归零、异常消失。同时也要学会另一面:不是拆得越细越好,拆过头查询全是连接,工程上要会在两端之间选位置。
函数依赖(FD)写作 :X 的值一旦确定,Y 的值唯一确定。「学号 → 姓名」(一个学号对应一个人名)、「(学号,课程号) → 成绩」(一对选课关系只有一个分数)、「系 → 系主任」。它必须是业务规则上恒成立的决定关系,不是「这个数据集里碰巧没重复」:如果恰巧今年没重名学生就认为「姓名 → 学号」,明年一个重名就翻车。
依赖分三种脾气,后面范式全靠它们分类:
「哪些属性组是候选码」可以不靠灵光一现,用闭包算法机械地算。 表示「从 X 出发,用已知 FD 能推出的一切属性」。算法就是不断扩张:
X⁺ = X
反复:找到某条 Y→Z,其中 Y ⊆ X⁺,把 Z 并入 X⁺
直到 X⁺ 不再变大
拿上面那表练一遍,已知 FD:学号→姓名、学号→系、系→系主任、(学号,课程号)→成绩:
(学号,课程号)⁺
= {学号, 课程号}
∪ {姓名} (学号→姓名) → {学号,课程号,姓名}
∪ {系} (学号→系) → +系
∪ {系主任}(系→系主任) → +系主任
∪ {成绩} ((学号,课程号)→成绩) → 全部属性
闭包覆盖全表所有属性,所以 (学号,课程号) 是码;再检查最小性:去掉课程号,学号⁺ 推不出成绩,不行;去掉学号更不行。结论:唯一候选码是 (学号,课程号)。闭包还能验证「某依赖是否被蕴含」:问「学号 → 系主任 成立吗」,算学号⁺ 看是否包含系主任即可。考试两大题型(求码、验证依赖)全用这一个算法。
范式是「拆到什么程度」的判据,一档比一档严格:
1NF:属性原子,一个格子一个值。「爱好」列里塞「篮球,游泳」就是典型违规,该拆成单独的爱好表(每行一个爱好)。
2NF:消除非主属性对码的部分依赖。把开头的表按闭包结论诊断:姓名、系、系主任只依赖学号(部分依赖),留在「学号+课程号」表里就是冗余。拆:
学生(学号, 姓名, 系, 系主任) ← 系主任仍在传递依赖里
选课(学号, 课程号, 成绩) ← 完全依赖,干净
3NF:再消除传递依赖。学生表里系主任经由「系」传递而来,再拆一刀:
学生(学号, 姓名, 系)
系(系, 系主任)
现在系主任全库只存一份,改主任改一行,三种异常全部消失。绝大多数业务表设计到 3NF 就是合理落点,这也是工程默认。
BCNF:更严一档,要求每个决定因素都是候选码(不只非主属性)。教科书经典反例:「学生选课由老师教」 且 ,拆到 3NF 仍有主属性冗余,BCNF 才干净。BCNF 偶尔与查询友好性冲突(拆掉某些连接),所以工程上「3NF 是默认,BCNF 是洁癖」,面试聊到这就够体现深度了。
范式拆表的无损性有数学保证(拆开的表用自然连接能精确还原),但这不等于「拆就完事」。每次拆细,查询就多一次连接:查「张三的系主任」从原来的一张表变成三表连接。读写比例决定了你在天平上的位置:
数据仓库甚至把反规范化推到极致(大宽表、星型模型),和分析型查询的访问模式配套。所以这门「设计理论」的正确打开方式:范式是体检指标,告诉你现在有多少冗余和异常风险;拆不拆、拆多少,最后是读写账本说了算。
{A→B, B→C, A→D}。求 A⁺;判断 A 是不是候选码;B→D 是否被 F 蕴含?销售(日期, 门店, 商品, 门店地址, 售价),FD:门店→门店地址、(日期,门店,商品)→售价。指出所有部分依赖和传递依赖,拆到 3NF。