「99% 的 Web 應用都在做同一件事:把資料存進去,再把資料取出來。 理解 SQL,就是理解這件事的本質。」
無論你用什麼框架、什麼語言,背後幾乎都有一個關聯式資料庫在撐腰。 這篇的目標不是讓你背完所有 SQL 語法,而是讓你真正理解「資料表之間的關係」, 以及為什麼 JOIN 是所有 SQL 的靈魂。
關聯式資料庫:用表格思考世界
關聯式資料庫(Relational Database)把所有資料組織成二維表格(Table)—— 就像 Excel,但更嚴格、更強大。每個表格有固定的欄位(Column),每一列是一筆資料(Row)。
範例:一個簡單的電商資料庫
users 表
| id (PK) | name | created_at | |
|---|---|---|---|
| 1 | Joseph | joseph@example.com | 2026-01-01 |
| 2 | Alice | alice@example.com | 2026-01-03 |
orders 表
| id (PK) | user_id (FK) | product | amount | status |
|---|---|---|---|---|
| 101 | 1 | MacBook Pro | 60000 | shipped |
| 102 | 1 | AirPods | 5000 | delivered |
| 103 | 2 | iPad | 25000 | pending |
💡 user_id 是「外鍵(Foreign Key)」——它指向 users 表的 id,這就是「關聯」的意義。
🔑
Primary Key (PK)
每一列的唯一識別符。不可重複、不可為 NULL。通常用自動遞增的整數(SERIAL / AUTO_INCREMENT)。
🔗
Foreign Key (FK)
指向另一張表 PK 的欄位,建立表之間的「關聯」。例如 orders.user_id 指向 users.id。
🛡️
Constraint(約束)
NOT NULL / UNIQUE / CHECK 等規則,在資料庫層確保資料合法,比在應用層驗證更可靠。
為什麼要把資料拆進多張表(正規化)?
如果把 user 的 name、email 都直接存進 orders 表,每次使用者改名, 你就必須更新所有相關訂單的記錄——這叫「更新異常(Update Anomaly)」。 把資料拆進獨立的 users 表,用 FK 關聯,改名時只需更新一筆。
這就是正規化(Normalization)的核心邏輯:每個事實只存一次,透過 FK 連接。 代價是查詢時需要 JOIN,但維護成本大幅降低。
DDL:定義你的資料結構
DDL(Data Definition Language)是用來建立和修改資料庫結構的語法。 先想清楚「資料長什麼樣子」,再動手建表——這個順序很重要。
SERIAL vs BIGSERIAL vs UUID:Primary Key 型別選哪個?
SERIAL32 位元整數,最大約 21 億。小型系統的預設選擇,最簡單。BIGSERIAL64 位元整數,最大約 922 京。資料量大或預期快速成長時使用。有序,索引效率高。UUID128 位元隨機字串(如 550e8400-...)。分散式系統中可在應用層生成不重複 ID,不需協調。代價:字串比較慢、索引膨脹。實務建議:單機/簡單系統用 BIGSERIAL;多服務、跨 DB 的分散式系統考慮 UUID(或 Snowflake ID)。
🔥 ON DELETE CASCADE 是什麼?
當你刪除一個 user 時,CASCADE 會自動刪除他所有的 orders。 另一個選項是 ON DELETE RESTRICT(預設)——如果 user 還有 orders,就拒絕刪除,強制你先處理子資料。
實務建議:大多數業務場景不應該真正刪除資料,改用「軟刪除」(加 deleted_at 欄位)更安全。
DML:CRUD 的四個操作
DML(Data Manipulation Language)是對資料本身的操作。Create / Read / Update / Delete, 幾乎所有應用邏輯都是這四個的組合。
WHERE 進階:篩選資料的藝術
WHERE vs HAVING 的差異
WHERE 在 GROUP BY 之前執行,用來過濾原始資料列。
HAVING 在 GROUP BY 之後執行,用來過濾分組的結果。
簡單記法:WHERE 過濾「行」,HAVING 過濾「組」。
JOIN:SQL 的靈魂
JOIN 是把兩張(或多張)表根據關聯欄位合併在一起查詢。 理解 JOIN 是 SQL 最重要的一關,也是面試必考的核心概念。
只回傳兩張表都有匹配的列。最常用。
左圓 ∩ 右圓(交集)
📌 使用情境:查詢有訂單的使用者
回傳左表所有列,右表沒匹配的填 NULL。
左圓全部 + 右圓交集部分
📌 使用情境:查詢所有使用者(含沒訂單的)
回傳右表所有列,左表沒匹配的填 NULL。(少用,通常可改成 LEFT JOIN)
右圓全部 + 左圓交集部分
📌 使用情境:查詢所有訂單(即使使用者已刪除)
回傳兩張表所有列,沒匹配的填 NULL。
左圓 ∪ 右圓(聯集)
📌 使用情境:對帳:找出任何一側缺漏的資料
⚠️ 常見陷阱:JOIN 後的 COUNT
LEFT JOIN 後,如果用 COUNT(*) 計算沒有訂單的使用者,會得到 1 而非 0, 因為那一列本身存在(只是 order 欄位都是 NULL)。
正確做法是 COUNT(o.id)——COUNT 遇到 NULL 自動跳過,結果才會是 0。
SQL 的執行順序(面試必考)
你寫 SQL 的順序是 SELECT → FROM → WHERE,但資料庫實際執行的順序完全不同。 理解這個順序,才能寫出正確的查詢(特別是 WHERE vs HAVING 的差異)。