Learning Objectives
- 從需求找出實體、屬性與實體之間的關聯,分辨一對多與多對多
- 說明主鍵、外鍵與實體、領域、參考三種完整性,並解讀外鍵擋下的錯誤(ERROR 1452、1451)
- 用多對多的中介表格(assignment、employeeSkills)表示「員工替客戶工作」與「員工有多項技能」
- 指出沒有正規化的表格會產生新增、更新、刪除異常
- 依第一到第三正規化的定義,把一張大表格拆成 department、employee、employeeSkills
為什麼要先設計
前兩課看到的 employee 資料庫有五張表格:department、employee、employeeSkills、client、assignment。
為什麼不把所有資料放在一張大表格裡,一列記一位員工的全部資料就好?
一張大表格看起來簡單,但同一份資訊(例如「部門 128 叫 Research and Development」)會在很多列裡重複出現。 重複的資料不只浪費空間,更麻煩的是它們可能彼此矛盾:改了其中一列、漏了另一列,資料庫就不知道哪一個才對。
資料庫設計的目標是:每一項事實只存一次,表格之間用「鍵」連起來,需要時再用查詢(第 07 課的 JOIN)組合回來。 這一課先建立設計的概念,再用正規化的步驟把一張大表格拆開。
實體、屬性與關聯
設計資料庫時,先從需求裡找出三樣東西:
| 名詞 | 意思 | 在 employee 資料庫 |
|---|---|---|
| 實體(entity) | 要記錄的「東西」,通常成為一張表格 | 部門、員工、客戶 |
| 屬性(attribute) | 實體的性質,成為表格的欄位 | 員工的編號、姓名、職稱 |
| 關聯(relationship) | 實體之間的關係 | 員工屬於部門;員工替客戶工作 |
關聯依照數量分成三種:
| 類型 | 例子 | 表格的做法 |
|---|---|---|
| 一對一(1:1) | 一位員工對應一張員工證 | 合併成一張表格,或其中一張用另一張的主鍵當外鍵 |
| 一對多(1:N) | 一個部門有很多員工,一位員工只屬於一個部門 | 在「多」的那一邊加外鍵:employee.departmentID |
| 多對多(M:N) | 一位員工可以替多個客戶工作,一個客戶也可以有多位員工 | 另外建一張中介表格,放兩邊的外鍵:assignment |
employee 資料庫的關聯可以畫成:
department 1 ──── N employee 1 ──── N employeeSkills
│
N
assignment(clientID、employeeID、workdate、hours)
N
│
client 1
「一位員工有多項技能」也是一對多:技能不是單一個值,所以另外放在 employeeSkills,一項技能一列。
主鍵、外鍵與三種完整性
主鍵(primary key)是能唯一識別一列的欄位或欄位組合。MySQL 對主鍵有兩個要求:值不可重複、不可以是 NULL。
department 的主鍵是 departmentID;employeeSkills 的主鍵是 (employeeID, skill) 兩個欄位的組合,稱為複合主鍵。
外鍵(foreign key)是「指向另一張表格主鍵」的欄位。employee.departmentID 指向 department.departmentID:
每位員工的部門編號,必須是 department 表格裡真的存在的部門。
原課程把資料的正確性整理成三種完整性(integrity):
| 完整性 | 規則 | MySQL 怎麼保證 |
|---|---|---|
| 實體完整性 | 每張表格有主鍵,主鍵唯一且不是 NULL | PRIMARY KEY;重複時出現 ERROR 1062 |
| 領域完整性 | 每個欄位的值合乎型別、格式與範圍 | 資料型別、NOT NULL、CHECK(第 04 課) |
| 參考完整性 | 外鍵指向的資料一定存在 | FOREIGN KEY;指向不存在的列出現 ERROR 1452,刪除還被參考的列出現 ERROR 1451 |
外鍵只在 InnoDB 儲存引擎有作用(MySQL 8.0 的預設引擎)。MyISAM 表格會接受外鍵的寫法,但不會檢查(第 09 課)。
主鍵與外鍵擋下的錯誤
先看 department 與 employee 兩張表格,再試著加入一位部門編號 999 的員工、刪除還有員工的部門 42、加入一個編號重複的部門。三個錯誤分別是參考完整性與實體完整性在把關。
多對多:中介表格
「員工替客戶工作」是多對多。如果在 employee 加一個 clientID 欄位,一位員工就只能有一個客戶;
在 client 加 employeeID,一個客戶就只能有一位員工。兩邊都不對。
做法是另外建一張中介表格 assignment,一列代表「某位員工在某一天替某個客戶工作幾小時」:
| 欄位 | 角色 |
|---|---|
clientID |
外鍵,指向 client |
employeeID |
外鍵,指向 employee |
workdate |
工作日期 |
hours |
工作時數,屬於這一次派工的資料 |
主鍵是 (clientID, employeeID, workdate) 三個欄位的組合:同一位員工可以在同一天替不同客戶工作,
也可以在不同天替同一個客戶工作,但「同一位員工、同一天、同一個客戶」只能有一列。
employeeSkills(employeeID, skill) 的結構也一樣:主鍵是兩個欄位的組合,同一位員工可以有很多技能,但同一項技能不會記兩次。
assignment 的複合主鍵
先看 Nora(7513)的三項技能,以及 client 與 assignment 的內容。接著新增兩筆 1 月 21 日替 The Bank(客戶 2)的派工:Ben 和 Nora 各一筆,都成功;再替 Ben 新增同一天、同一個客戶的第二筆,就違反了複合主鍵。
想一想:鍵與關聯
下面哪一個欄位不適合當作 employee 表格的主鍵?
在 employee 資料庫執行下面的敘述,會得到什麼?(部門 999 不存在)
INSERT INTO employee VALUES (5001, 'Mia Chen', 'Analyst', 999);
學生可以修很多門課,一門課也有很多學生,成績屬於「某位學生修某門課」。成績應該放在哪裡?
沒有正規化的表格:三種異常
假設把員工資料全部放進一張表格:
employee_flat(employeeID, name, job, departmentID, departmentName, skills)
departmentID 決定了 departmentName:128 一定是 Research and Development,42 一定是 Finance。
但部門名稱跟著每位員工重複存了好幾次,於是產生三種異常(anomaly):
| 異常 | 情況 |
|---|---|
| 新增異常 | 新增員工時要再打一次部門名稱,打錯(例如 42 打成 Research & Development)資料就矛盾;還沒有員工的新部門(129 Human Resources)根本記不進去,因為主鍵 employeeID 不能是 NULL |
| 更新異常 | 部門 128 改名為 Emerging Technologies,只改了其中一列,同一個部門就有兩個名稱 |
| 刪除異常 | 刪除 Ben Smith(部門 42 唯一的員工),連「部門 42 叫 Finance」這項事實也一起消失 |
另外,skills 欄位把好幾項技能寫在同一格('C, Perl, Java'),想找「會 Java 的員工」只能用 LIKE '%Java%',
連 JavaScript 也會被找出來;想刪掉其中一項技能,也得改寫整個字串。
親眼看看三種異常
執行前已經在 design 資料庫建好 employee_flat,資料和 employee 資料庫相同,只是全部擠在一張表格裡。注意右邊表格的 departmentName 欄位:每執行一個敘述,部門名稱的一致性就被破壞一點。
正規化:第一到第三正規化
正規化(normalization)是一套逐步拆分表格的規則,每一步消除一種會造成異常的相依關係。
先說明功能相依:如果知道 A 的值就能確定 B 的值,稱為「B 相依於 A」,寫成 A → B。例如 departmentID → departmentName。
| 正規化 | 條件 | 做法 |
|---|---|---|
| 第一正規化(1NF) | 每一格只放一個值(atomic),沒有重複的欄位群組 | 一格多個值的欄位,拆成多列 |
| 第二正規化(2NF) | 符合 1NF,而且非鍵欄位完全相依於整個主鍵,不能只相依於複合主鍵的一部分 | 把部分相依的欄位移到新表格 |
| 第三正規化(3NF) | 符合 2NF,而且非鍵欄位只相依於主鍵,不能透過另一個非鍵欄位間接相依(遞移相依) | 把遞移相依的欄位移到新表格 |
以投影片的例子一步一步做:
原始表格:employee(employeeID, name, job, departmentID, departmentName, skill),skill 一格放好幾項技能。
1NF:skill 拆成一項一列,主鍵變成 (employeeID, skill)。Nora 有 3 項技能,就變成 3 列,姓名、部門跟著重複 3 次。
2NF:主鍵是 (employeeID, skill),但 name、job、departmentID、departmentName 只要知道 employeeID 就確定了,
和 skill 無關,這是部分相依。把它們移出去:
employeeSkills(employeeID, skill)employee(employeeID, name, job, departmentID, departmentName)
3NF:在新的 employee 表格裡,employeeID → departmentID,而 departmentID → departmentName,
所以 departmentName 是經由 departmentID 間接相依於主鍵,這是遞移相依。把部門名稱移出去:
employee(employeeID, name, job, departmentID)department(departmentID, departmentName)
結果就是 employee 資料庫的三張表格。原本的投影片把「拆出 department」放在第二正規化; 嚴格依照定義,它是遞移相依,屬於第三正規化,最後拆出來的表格相同。
正規化到 3NF 之後,每一項事實只存在一個地方:部門名稱只在 department 存一次,改名只要改一列,沒有員工的部門也記得進去。
把 1NF 的表格拆成三張表格
執行前已經建好符合 1NF 的 employee_1nf(一項技能一列,共 8 列)。依序建立 department、employee、employeeSkills,用 INSERT … SELECT 從原表格複製資料:DISTINCT 讓重複的部門、重複的員工只留一列。因為外鍵要指向已經存在的表格,建立的順序是 department → employee → employeeSkills。最後一行驗證拆開之後的外鍵確實在把關。
想一想:異常與正規化
employee_flat 原本有 2 種部門編號與名稱的組合。執行下面的 UPDATE 之後,SELECT DISTINCT departmentID, departmentName 會得到幾列?
UPDATE employee_flat SET departmentName = 'Emerging Technologies'
WHERE employeeID = 7513;
(部門 128 有 3 位員工,7513 是其中一位)
student(studentID, name, phones) 的 phones 欄位存的是 '0912-345678, 03-4227151'。這張表格違反哪一個正規化?
在 employee(employeeID, name, job, departmentID, departmentName) 中,departmentName 和主鍵 employeeID 是什麼關係?
練習:訂單表格
投影片的練習題是一張訂單表格:
order(date, customerID, customerName, productID, productName, price, quantity)
找出功能相依:
customerID → customerName:知道顧客編號就知道顧客姓名productID → productName, price:知道商品編號就知道品名與單價- quantity 要知道「哪一筆訂單的哪一項商品」才能確定
拆開之後:
customer(customerID, customerName)product(productID, productName, price)orders(orderID, orderDate, customerID, productID, quantity):新增流水號 orderID 當主鍵
這是投影片的答案,它假設一張訂單只買一種商品。如果一張訂單可以買好幾種商品, orderDate 與 customerID 只相依於 orderID,quantity 卻相依於 (orderID, productID),還要再拆成:
orders(orderID, orderDate, customerID)orderItems(orderID, productID, quantity),主鍵是(orderID, productID)
另外,order 是 SQL 的保留字(ORDER BY),拿來當表格名稱每次都得加反引號,所以改叫 orders。
動手寫:拆出 customer 表格
已經建好 shop 資料庫的 order_flat 表格(5 列訂單明細,同一位顧客的姓名重複出現)。請建立 customer 表格(customerID CHAR(3) 主鍵、customerName VARCHAR(20) NOT NULL),用 INSERT … SELECT DISTINCT 從 order_flat 複製顧客資料,最後依 customerID 排序列出 customer。
重點整理
| 概念 | 重點 |
|---|---|
| 實體、屬性、關聯 | 實體成為表格,屬性成為欄位;一對多在「多」的一邊放外鍵,多對多用中介表格 |
| 主鍵 | 唯一、不是 NULL;可以是多個欄位的組合(employeeSkills、assignment) |
| 外鍵 | 指向另一張表格的主鍵;新增不存在的值 ERROR 1452,刪除還被參考的列 ERROR 1451 |
| 異常 | 資料重複存放造成新增、更新、刪除異常 |
| 1NF | 一格一個值 |
| 2NF | 沒有部分相依(非鍵欄位相依於整個主鍵) |
| 3NF | 沒有遞移相依(非鍵欄位只相依於主鍵) |
正規化不是越拆越好:拆開之後查詢要用 JOIN 組回來。實務上通常做到 3NF, 少數為了查詢效能刻意保留重複資料的做法稱為反正規化(denormalization),要清楚知道代價才使用。
下一課用 CREATE DATABASE、CREATE TABLE 把這個設計實際建立起來。