一個設計混亂的數據庫就像一個雜亂的房間,用起來非常不方便:東西到處亂放(數據冗余),找件東西要翻遍所有角落(查詢困難),扔掉一把舊椅子時,可能會把搭在上面的唯一一件外套也一起扔了(刪除異常)。
數據庫范式(Normalization) 就是一套“房間整理法則”,指導我們如何合理地組織數據表,以消除冗余、避免上述問題。2NF和3NF是其中最核心、最重要的兩條法則。
初始狀態:亂糟糟的第一范式 (1NF)
假設我們要設計一個系統來記錄學生的選課成績。最直接的想法可能是把所有信息都塞進一張大表里。這張表符合第一范式(1NF),因為每個單元格都只有一個值,并且沒有完全重復的行。但你會發現,它問題重重。
選課成績表 (符合1NF)
學號 | 課程號 | 學生姓名 | 課程名稱 | 授課老師 | 成績 |
---|---|---|---|---|---|
101 | CS101 | 張三 | 計算機導論 | 王老師 | 92 |
101 | MTH202 | 張三 | 高等數學 | 李老師 | 85 |
102 | CS101 | 李四 | 計算機導論 | 王老師 | 88 |
103 | PHY301 | 王五 | 量子力學 | 趙老師 | 95 |
我們先來分析這張表的“身份”信息:
- 主碼 (Primary Key): 要唯一確定一行數據,需要同時知道“哪個學生”和“哪門課”。因為一個學生可以選多門課,一門課也可以被多個學生選。所以,主碼是
(學號, 課程號)
這樣一個組合。 - 主屬性:
學號
,課程號
。 - 非主屬性:
學生姓名
,課程名稱
,授課老師
,成績
。
這張表雖然能用,但有嚴重的“副作用”(我們稱之為“異常”):
- 更新異常 (Update Anomaly): 如果學生“張三”改名叫“張大三”,你必須修改所有他出現的行。一旦漏掉一行,數據就不一致了。
- 插入異常 (Insertion Anomaly): 你無法添加一個新生(比如學號104的“趙六”),除非他至少選了一門課。因為主碼
(學號, 課程號)
要求課程號
不能為空。 - 刪除異常 (Deletion Anomaly): 如果“王五”退掉了他唯一選的課(量子力學),那么他所在的整行數據都會被刪除。我們不僅丟失了他的選課記錄,連“王五”這個學生本身的信息也從數據庫里消失了!
如何解決這些問題?答案就是遵循范式法則進行整理。
第一步整理:達到第二范式 (2NF)
2NF的法則
一個表在符合1NF的基礎上,如果所有非主屬性都“完全函數依賴”于主碼,那么它就符合2NF。
- “完全函數依賴”:聽起來很學術,其實很簡單。意思就是一個非主屬性的值,必須由整個主碼來唯一確定,而不是由主碼的一部分就能確定。
- 通俗版法則:任何一個非主屬性,都不能只依賴于組合主碼的一部分。(這條規則只在主碼是組合碼時才有意義,就像我們這個例子)。
運用2NF法則整理
我們的主碼是 (學號, 課程號)
。我們來檢查一下每個非主屬性:
成績
: 由什么決定?必須同時知道學號
和課程號
才能確定一個唯一的成績。所以,成績
依賴于整個主碼。這很好,符合2NF。學生姓名
: 由什么決定?只需要學號
就能確定。學生的姓名和他選了什么課沒關系。所以,學生姓名
只依賴于主碼的一部分 (學號
)。這很糟糕,違反了2NF。課程名稱
和授課老師
: 由什么決定?只需要課程號
就能確定。這和哪個學生來選課也沒關系。所以,這兩個屬性也只依賴于主碼的一部分 (課程號
)。這也很糟糕,違反了2NF。
解決方法:拆分!
解決辦法就是“分家”。把那些只依賴于部分主碼的信息,拆分出去成立自己的獨立小家庭(新表)。
- 只依賴于
學號
的信息,放到新的學生表
里。 - 只依賴于
課程號
的信息,放到新的課程表
里。 - 依賴于
(學號, 課程號)
完整組合的信息,留在瘦身后的選課表
里。
學生表 (Students)
學號 (主碼) | 學生姓名 |
---|---|
101 | 張三 |
102 | 李四 |
103 | 王五 |
課程表 (Courses)
課程號 (主碼) | 課程名稱 | 授課老師 |
---|---|---|
CS101 | 計算機導論 | 王老師 |
MTH202 | 高等數學 | 李老師 |
PHY301 | 量子力學 | 趙老師 |
選課表 (Enrollment)
學號 (外碼) | 課程號 (外碼) | 成績 |
---|---|---|
101 | CS101 | 92 |
101 | MTH202 | 85 |
102 | CS101 | 88 |
103 | PHY301 | 95 |
(這張表的主碼依然是 (學號, 課程號)) |
現在,我們所有的表都符合2NF了,之前提到的那些異常問題也隨之解決!張三改名,只需改 學生表
一處。新生趙六可以直接添加到 學生表
,不用非得選課。
第二步整理:達到第三范式 (3NF)
我們離完美還差一步。仔細看剛剛創建的 課程表
,它里面還隱藏著一個問題。
3NF的法則
一個表在符合2NF的基礎上,如果不存在“傳遞函數依賴”,那么它就符合3NF。
- “傳遞函數依賴”:指的是一個非主屬性,不直接依賴于主碼,而是通過另一個非主屬性間接地依賴于主碼。形成了一個依賴鏈條:
主碼 -> 非主屬性A -> 非主屬性B
。 - 通俗版法則:任何一個非主屬性,都不能依賴于其他非主屬性。
運用3NF法則整理
為了讓問題更明顯,我們在 課程表
里加一列“老師所在院系”。
課程表 (符合2NF,但不符合3NF)
課程號 (主碼) | 課程名稱 | 授課老師 | 老師所在院系 |
---|---|---|---|
CS101 | 計算機導論 | 王老師 | 計算機系 |
MTH202 | 高等數學 | 李老師 | 數學系 |
CS205 | 數據結構 | 王老師 | 計算機系 |
我們來檢查主碼 課程號
和非主屬性之間的依賴關系:
課程名稱
直接依賴于課程號
。很好。授課老師
直接依賴于課程號
(為簡化,假設一門課只有一個老師)。很好。老師所在院系
: 老師的院系是由課程號
決定的嗎?不是! 它是由授課老師決定的。這樣就形成了一個依賴鏈條:
課程號
->授課老師
->老師所在院系
這就是傳遞依賴,它違反了3NF。
這又會引發老問題!如果“王老師”從“計算機系”調到了“人工智能系”,你就必須修改他教的每一門課的記錄,非常麻煩且容易出錯。
解決方法:再次拆分!
我們把這個傳遞依賴鏈條也拆開,把老師的信息獨立出去。
老師表 (Instructors)
授課老師 (主碼) | 老師所在院系 |
---|---|
王老師 | 計算機系 |
李老師 | 數學系 |
趙老師 | 物理系 |
新的課程表 (Courses)
課程號 (主碼) | 課程名稱 | 授課老師 (外碼) |
---|---|---|
CS101 | 計算機導論 | 王老師 |
MTH202 | 高等數學 | 李老師 |
CS205 | 數據結構 | 王老師 |
PHY301 | 量子力學 | 趙老師 |
現在,如果王老師調動院系,我們只需在 老師表
中修改唯一的一條記錄。至此,我們所有的表都達到了3NF,房間整理完畢!
最終總結
- 1NF:入場券。 確保數據是“原子的”(每個單元格一個值)。
- 2NF:消除部分依賴。 確保所有非主屬性都依賴于整個組合主碼,而不是它的一部分。(解決方法:把依賴于部分的屬性拆出去)。
- 3NF:消除傳遞依賴。 確保所有非主屬性都只依賴于主碼,而不是依賴于其他非主屬性。(解決方法:把間接依賴的屬性拆出去)。
通過遵循這些法則,我們把一張龐大、混亂的表,拆分成了四個干凈、清晰、健壯的小表:學生表
、老師表
、課程表
和 選課表
。這就是關系數據庫設計的精髓所在。