跳到内容

3.2 函数依赖、范式与可验证分解

同一位居民的地址散落在多张登记表里,修改一处后其他副本仍保留旧值,档案城开始出现互相矛盾的事实。

“不要重复数据”只是 normalization 的直觉,不是定义。规范化从 functional dependency 出发,判断 attribute 是否由正确的 key 决定,并检查 decomposition 是否 lossless、是否保留重要 dependencies。

一个混合 relation

假设存在:

text
QuestAssignment(
  quest_id,
  item_id,
  adventurer_id,
  adventurer_name,
  item_name,
  quantity,
  reward_cents
)

业务规则给出 functional dependencies:

text
quest_id -> adventurer_id, reward_cents
adventurer_id -> adventurer_name
item_id -> item_name
(quest_id, item_id) -> quantity

若一项 quest 可以有多种 item,则 candidate key 是 (quest_id, item_id)。把 adventurer/item 名称重复放在每个 assignment row,会产生 update、insert 与 delete anomaly。

Functional dependency 的含义

X -> Y 表示:在所有合法 relation states 中,任意两 tuples 只要 X 相同,Y 就必须相同。

它是业务 invariant,不是从当前 sample data 猜出来的 correlation。今天每个 name 恰好唯一,不足以证明 name -> id;必须由业务规则保证所有未来合法状态也成立。

Trivial 与 non-trivial

Y ⊆ XX -> Y 是 trivial,例如 (quest_id, item_id) -> quest_id。规范化主要关注 non-trivial dependencies。

Attribute closure 与 candidate key

使用 Armstrong axioms 可以推导 dependency:

  • reflexivity:若 Y ⊆ X,则 X -> Y
  • augmentation:若 X -> Y,则 XZ -> YZ
  • transitivity:若 X -> YY -> Z,则 X -> Z

(quest_id, item_id) 求 closure:

text
start: quest_id, item_id
quest_id -> adventurer_id, reward_cents
adventurer_id -> adventurer_name
item_id -> item_name
(quest_id, item_id) -> quantity

closure = all attributes

所以它是 superkey;如果删除其中任一 attribute 都不能决定全部 attributes,它还是 candidate key。

一个 relation 可以有多个 candidate keys。选为 PRIMARY KEY 的是其中一个,其他业务 candidate keys 仍应通过 UNIQUE + NOT NULL 等方式表达。

1NF:不要把它简化成“不能有数组”

First Normal Form 要求 relation attribute 在关系模型采用的 domain 中取 atomic value,并且没有 repeating groups。atomic 与具体 domain/查询语义有关:数据库可以把 JSON 当作一个值,但如果业务要对其中 item IDs 做 FK、join 和独立更新,那么把它当 opaque scalar 会失去关系约束。

判断时看内部结构是否属于当前 relation model 需要独立操作的事实,存储类型的名字并不重要。

2NF:消除对 candidate key 的 partial dependency

2NF 要求 relation 已在 1NF,且每个 non-prime attribute fully functionally depends on every candidate key。

在 composite key (quest_id, item_id) 下:

text
quest_id -> reward_cents
item_id  -> item_name

它们只依赖 key 的一部分,违反 2NF。分出:

text
Quests(quest_id, adventurer_id, reward_cents)
Items(item_id, item_name)
QuestItems(quest_id, item_id, quantity)

若 relation 的每个 candidate key 都只有一个 attribute,就不会有 partial dependency,因此自动满足 2NF;但仍可能违反 3NF/BCNF。

3NF:处理 transitive dependency

Quests(quest_id, adventurer_id, adventurer_name, reward_cents) 中:

text
quest_id -> adventurer_id
adventurer_id -> adventurer_name

于是 quest_id -> adventurer_name 是 transitive dependency。adventurer_name 描述 adventurer,不应由每项 quest 重复维护:

text
Adventurers(adventurer_id, adventurer_name)
Quests(quest_id, adventurer_id, reward_cents)

正式 3NF 条件:对每个 non-trivial FD X -> A,X 是 superkey,或 A 是 prime attribute(属于某个 candidate key)。口诀“非 key 只依赖 key”帮助入门,但处理多个 candidate keys 时不够精确。

BCNF:每个 determinant 都是 superkey

BCNF 要求对每个 non-trivial FD X -> Y,X 都是 superkey。它比 3NF 更严格。

某些 relation 可以满足 3NF 但不满足 BCNF,因为 RHS 是 prime attribute。分解到 BCNF 可能无法保留所有 dependencies,于是设计者要在 stronger redundancy control 与 dependency preservation 之间取舍。

“3NF 一定够用”与“所有表必须 BCNF”都不是专业结论。应列出 dependencies、证明 decomposition properties,并结合 constraint 能力决策。

Lossless join 是分解底线

把 relation R 分解成 R1、R2 后,natural join 应能恢复恰好原 relation,而不是产生 spurious tuples 或丢失事实。

对 binary decomposition,一个常用判据是交集 attributes 能 functionally determine R1 或 R2:

text
(R1 ∩ R2) -> R1
or
(R1 ∩ R2) -> R2

例如:

text
R(quest_id, adventurer_id, adventurer_name)
R1(quest_id, adventurer_id)
R2(adventurer_id, adventurer_name)

交集是 adventurer_id,且 adventurer_id -> adventurer_name,所以这个 binary decomposition 是 lossless。

Dependency preservation

若分解后每个原 dependency 都能通过单个 relation constraints 或其投影组合检查,而无需 join 全部 relation,则 dependency-preserving。

lossless 与 dependency preservation 是两个不同属性。一个 decomposition 可以 lossless,却让某项 dependency 只能跨表检查;这会提高 constraint 实现成本。

映射成 SQL schema

sql
CREATE TABLE adventurers (
    adventurer_id   INTEGER PRIMARY KEY,
    adventurer_name TEXT NOT NULL
);

CREATE TABLE items (
    item_id   INTEGER PRIMARY KEY,
    item_name TEXT NOT NULL
);

CREATE TABLE quests (
    quest_id      INTEGER PRIMARY KEY,
    adventurer_id INTEGER NOT NULL,
    reward_cents  INTEGER NOT NULL CHECK (reward_cents >= 0),
    FOREIGN KEY (adventurer_id)
        REFERENCES adventurers(adventurer_id)
);

CREATE TABLE quest_items (
    quest_id INTEGER NOT NULL,
    item_id  INTEGER NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    PRIMARY KEY (quest_id, item_id),
    FOREIGN KEY (quest_id) REFERENCES quests(quest_id),
    FOREIGN KEY (item_id) REFERENCES items(item_id)
);

这个 schema 表达了列出的 dependencies,但还没有表达 status transition、历史价格或 tenant isolation;normalization 不替代完整 domain modeling。

Denormalization 是受控复制

为读性能复制 adventurer_name、保存汇总表或构建 materialized view 可能合理,但要先回答:

  • source of truth 是谁;
  • 何时同步:同 transaction、async event、periodic refresh;
  • 可容忍多长 staleness;
  • 失败如何补偿/rebuild;
  • 如何检测 drift;
  • 是否真的比 index/query rewrite/cache 更合适。

denormalization 指在理解 dependency 后有意维护额外 representation,并非抛弃范式。

不要用范式解决所有问题

Normalization 主要处理 dependency 与 redundancy。以下问题需要其他机制:

  • concurrent write anomalies:transaction isolation/concurrency control;
  • append-only audit:history/event model;
  • analytics performance:columnar layout/materialization;
  • distributed consistency:replication/consensus/transaction protocol;
  • authorization:policy and access control;
  • schema evolution:migration/backfill/compatibility。

练习

给定:

text
Enrollment(student_id, course_id, instructor_id,
           student_name, course_title, instructor_office, grade)

以及:

text
student_id -> student_name
course_id -> course_title, instructor_id
instructor_id -> instructor_office
(student_id, course_id) -> grade
  1. (student_id, course_id)+
  2. 找出 partial 与 transitive dependencies。
  3. 分解到 3NF。
  4. 说明每一步为什么 lossless。
  5. 列出 keys/FKs/UNIQUE/CHECK,指出仍无法直接用这些 constraint 保证的规则。

验收标准

  • [ ] FD 来自业务 invariant,不从样本偶然性猜测;
  • [ ] 能用 closure 识别 candidate key;
  • [ ] 能精确定义 2NF、3NF 与 BCNF;
  • [ ] decomposition 检查 lossless 与 dependency preservation;
  • [ ] denormalization 写明 source、freshness、repair;
  • [ ] 不把 normalization 与 transaction isolation 混为一谈。

本章小结

ER modeling 决定系统要记录哪些 identity 与 relationship,functional dependencies 则检查每个 relation 是否混合了不同决定因素。好的 schema 让业务事实、key、dependency 和 lifecycle 都有清晰归属,并尽量用 constraint 与 transaction 执行这些约束;表的数量并非目标。

Built with VitePress | Software Systems Atlas