關聯式資料庫核心觀念
關聯式資料庫(Relational Database)把資料存放在多張「資料表」中,並透過共同的欄位把資料表串起來。理解資料表、主鍵、外鍵、正規化、交易與索引這六個觀念,就能看懂絕大多數資料庫的設計,也能避免資料重複與不一致的問題。
資料表、列與欄
資料表(Table)就像一張試算表工作表,但結構更嚴格:
- 欄(Column):定義資料的屬性與型別,例如
name TEXT、age INTEGER。每一欄只能放同一種型別的資料。 - 列(Row):一筆完整的資料,例如「Alice,30 歲」。
- 結構(Schema):資料表有哪些欄位、各欄位的型別與限制,事先定義好。
| id | name | age |
|---|---|---|
| 1 | Alice | 30 |
| 2 | Bob | 24 |
與試算表最大的不同是:資料庫會強制檢查資料是否符合結構,例如年齡欄位不能填入文字、必填欄位不能空白,因此資料品質比較有保障。
主鍵與外鍵
主鍵(Primary Key)
主鍵是唯一識別每一列的欄位,值不能重複也不能是空值(NULL)。最常見的做法是用自動遞增的整數 id。有了主鍵,「修改 id = 2 那位使用者」這類操作才不會改錯人。
外鍵(Foreign Key)
外鍵是指向另一張表主鍵的欄位,用來表示兩張表之間的關係。例如訂單表的 user_id 指向使用者表的 id:
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
amount INTEGER NOT NULL
);
資料庫會檢查外鍵的值是否真的存在於 users 表中,避免出現「訂單屬於一個不存在的使用者」這類孤兒資料。
SQLite 預設不檢查外鍵
SQLite 需要在每次連線後執行 PRAGMA foreign_keys = ON; 才會強制檢查外鍵;PostgreSQL 與 MySQL(InnoDB)則預設啟用。
資料表之間的三種關係
| 關係 | 例子 | 設計方式 |
|---|---|---|
| 一對一 | 使用者 ↔ 個人檔案 | 其中一張表放外鍵,並加上 UNIQUE |
| 一對多 | 使用者 → 多筆訂單 | 在「多」的那一方(訂單)放外鍵 |
| 多對多 | 學生 ↔ 課程 | 另建一張中介表(例如 enrollments),放兩個外鍵 |
erDiagram
USERS ||--o{ ORDERS : "下訂"
STUDENTS ||--o{ ENROLLMENTS : "選修"
COURSES ||--o{ ENROLLMENTS : "被選"
正規化:為什麼要拆表?
正規化(Normalization)是把資料拆到多張表、讓同一件事只記錄在一個地方的設計方法。如果把使用者姓名直接寫在每一筆訂單裡,當使用者改名時就得修改所有訂單,漏改一筆就會產生不一致。
實務上記住前三個正規化形式(Normal Form)的精神即可:
- 第一正規化(1NF):每一格只放一個值,不要在一格裡塞「A, B, C」這種清單。
- 第二正規化(2NF):非主鍵欄位必須依賴整個主鍵,而不是主鍵的一部分。
- 第三正規化(3NF):非主鍵欄位不能依賴其他非主鍵欄位,例如「郵遞區號 → 城市」應該拆成另一張表。
反正規化
正規化會增加 JOIN 的次數。報表或讀取量極大的系統,有時會刻意保留部分重複資料(反正規化,Denormalization)來換取查詢速度,但這應該是量測過效能之後的決定。
交易與 ACID
交易(Transaction)是一組必須「全部成功或全部取消」的操作。最經典的例子是轉帳:從 A 帳戶扣款與存入 B 帳戶必須同時成功,否則錢就憑空消失。
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 若中途出錯,改執行 ROLLBACK; 取消全部變更
關聯式資料庫用 ACID 四個特性保證交易可靠:
| 特性 | 英文 | 意義 |
|---|---|---|
| 原子性 | Atomicity | 交易內的操作全部完成或全部不做 |
| 一致性 | Consistency | 交易前後資料都符合所有規則(主鍵、外鍵、限制條件) |
| 隔離性 | Isolation | 同時進行的交易互不干擾,看不到彼此未完成的變更 |
| 持久性 | Durability | 交易一旦 COMMIT,即使當機或斷電資料也不會遺失 |
這也是 Python 程式寫入資料後一定要呼叫 conn.commit() 的原因:沒有 commit,變更只存在於尚未完成的交易中。
索引:讓查詢變快
索引(Index)就像書末的索引頁。沒有索引時,資料庫必須逐列掃描整張表(Full Table Scan);建立索引後,可以直接跳到符合條件的位置。
- 該建索引的欄位:常出現在
WHERE、JOIN、ORDER BY中的欄位。主鍵與UNIQUE欄位會自動建立索引。 - 代價:索引會佔用空間,並讓
INSERT、UPDATE、DELETE變慢,因為每次寫入都要同步更新索引。所以不是越多越好。 - 檢查方式:在查詢前加上
EXPLAIN(SQLite 為EXPLAIN QUERY PLAN),可以看到資料庫是否真的用到了索引。
推薦影音
Learn Database Normalization - 1NF 到 5NF(Decomplexify)
簡述:用大量實例一步一步示範如何把一張雜亂的表正規化,是 YouTube 上講解正規化最清楚的影片之一,對應本頁「正規化」一節。
Relational Database ACID Transactions(Hussein Nasser)
簡述:後端工程師 Hussein Nasser 以實際範例拆解 ACID 四個特性與隔離等級(Isolation Level)會遇到的讀取異常,適合想深入理解「交易」的讀者。
Database Indexing Explained(Hussein Nasser)
簡述:在 PostgreSQL 上實際建立索引並比較查詢計畫(Query Plan),直觀看見索引帶來的速度差異,對應本頁「索引」一節。