EP.01資料庫與 SQL

SQL 入門:SELECT 不只是查資料
從關聯式資料庫到 JOIN 的完整思維

資料庫是每個軟體系統的心臟。學 SQL 不只是背指令, 更要理解「為什麼要這樣設計」。本篇從零開始,帶你建立正確的資料庫思維。

Joseph Chen 2026 14 min read SQL · CRUD · JOIN · PostgreSQL

「99% 的 Web 應用都在做同一件事:把資料存進去,再把資料取出來。 理解 SQL,就是理解這件事的本質。」

無論你用什麼框架、什麼語言,背後幾乎都有一個關聯式資料庫在撐腰。 這篇的目標不是讓你背完所有 SQL 語法,而是讓你真正理解「資料表之間的關係」, 以及為什麼 JOIN 是所有 SQL 的靈魂。

關聯式資料庫:用表格思考世界

關聯式資料庫(Relational Database)把所有資料組織成二維表格(Table)—— 就像 Excel,但更嚴格、更強大。每個表格有固定的欄位(Column),每一列是一筆資料(Row)。

範例:一個簡單的電商資料庫

users 表

id (PK)nameemailcreated_at
1Josephjoseph@example.com2026-01-01
2Alicealice@example.com2026-01-03

orders 表

id (PK)user_id (FK)productamountstatus
1011MacBook Pro60000shipped
1021AirPods5000delivered
1032iPad25000pending

💡 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)是用來建立和修改資料庫結構的語法。 先想清楚「資料長什麼樣子」,再動手建表——這個順序很重要。

建立 users 和 orders 資料表(PostgreSQL)
-- 建立 users 表
CREATE TABLE users (
    id         SERIAL PRIMARY KEY,          -- 自動遞增 ID
    name       VARCHAR(100) NOT NULL,       -- 最多 100 字元,不可為空
    email      VARCHAR(255) UNIQUE NOT NULL, -- 唯一且不可為空
    created_at TIMESTAMP DEFAULT NOW()      -- 預設為當下時間
);

-- 建立 orders 表(有外鍵關聯)
CREATE TABLE orders (
    id         SERIAL PRIMARY KEY,
    user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    product    VARCHAR(200) NOT NULL,
    amount     DECIMAL(10, 2) NOT NULL CHECK (amount > 0),
    status     VARCHAR(20) DEFAULT 'pending'
               CHECK (status IN ('pending', 'shipped', 'delivered')),
    created_at TIMESTAMP DEFAULT NOW()
);

-- 常用的 DDL 操作
ALTER TABLE users ADD COLUMN phone VARCHAR(20);  -- 新增欄位
ALTER TABLE users DROP COLUMN phone;              -- 刪除欄位
DROP TABLE orders;                                -- 刪除整張表(謹慎!)

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, 幾乎所有應用邏輯都是這四個的組合。

CRUD 基本操作
-- ─────────────── CREATE(新增)───────────────
INSERT INTO users (name, email)
VALUES ('Joseph', 'joseph@example.com');

-- 一次插入多筆
INSERT INTO orders (user_id, product, amount)
VALUES (1, 'MacBook Pro', 60000),
       (1, 'AirPods', 5000);

-- ─────────────── READ(查詢)────────────────
SELECT * FROM users;                    -- 所有欄位(避免在 production 用 *)
SELECT id, name, email FROM users;     -- 指定欄位

-- WHERE 條件
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE amount > 10000 AND status = 'shipped';

-- ORDER BY / LIMIT / OFFSET(分頁)
SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 10 OFFSET 0;   -- 第一頁,每頁 10 筆

-- ─────────────── UPDATE(修改)───────────────
UPDATE orders
SET status = 'shipped'
WHERE id = 101;

-- ⚠️ 千萬記得加 WHERE,不然會更新所有列!
-- UPDATE orders SET status = 'shipped';   -- 災難!

-- ─────────────── DELETE(刪除)───────────────
DELETE FROM orders WHERE id = 101;

-- 軟刪除(推薦做法)
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP;
UPDATE users SET deleted_at = NOW() WHERE id = 1;
-- 查詢時過濾掉已刪除的
SELECT * FROM users WHERE deleted_at IS NULL;

WHERE 進階:篩選資料的藝術

WHERE 常用技巧
-- LIKE:模糊搜尋(% 匹配任意字元,_ 匹配單一字元)
SELECT * FROM users WHERE email LIKE '%@gmail.com';
SELECT * FROM users WHERE name LIKE 'J%';   -- 名字以 J 開頭

-- IN:多值比對(比多個 OR 更清晰)
SELECT * FROM orders WHERE status IN ('pending', 'shipped');
-- 等同於 WHERE status = 'pending' OR status = 'shipped'

-- BETWEEN:範圍查詢(含兩端)
SELECT * FROM orders WHERE amount BETWEEN 1000 AND 50000;

-- IS NULL / IS NOT NULL
SELECT * FROM users WHERE deleted_at IS NULL;

-- 子查詢(Subquery):在 WHERE 中嵌套另一個 SELECT
SELECT * FROM orders
WHERE user_id IN (
    SELECT id FROM users WHERE email LIKE '%@company.com'
);

-- 聚合函數:COUNT / SUM / AVG / MAX / MIN
SELECT COUNT(*) FROM orders WHERE status = 'pending';
SELECT SUM(amount) FROM orders WHERE user_id = 1;
SELECT AVG(amount) FROM orders;

-- GROUP BY + HAVING(分組後再篩選)
SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total
FROM orders
GROUP BY user_id
HAVING SUM(amount) > 10000;   -- HAVING 是對分組後的結果篩選(不是 WHERE)

WHERE vs HAVING 的差異

WHERE 在 GROUP BY 之前執行,用來過濾原始資料列。
HAVING 在 GROUP BY 之後執行,用來過濾分組的結果。

簡單記法:WHERE 過濾「行」,HAVING 過濾「組」。


JOIN:SQL 的靈魂

JOIN 是把兩張(或多張)表根據關聯欄位合併在一起查詢。 理解 JOIN 是 SQL 最重要的一關,也是面試必考的核心概念。

INNER JOIN

只回傳兩張表都有匹配的列。最常用。

左圓 ∩ 右圓(交集)

📌 使用情境:查詢有訂單的使用者

LEFT JOIN

回傳左表所有列,右表沒匹配的填 NULL。

左圓全部 + 右圓交集部分

📌 使用情境:查詢所有使用者(含沒訂單的)

RIGHT JOIN

回傳右表所有列,左表沒匹配的填 NULL。(少用,通常可改成 LEFT JOIN)

右圓全部 + 左圓交集部分

📌 使用情境:查詢所有訂單(即使使用者已刪除)

FULL OUTER JOIN

回傳兩張表所有列,沒匹配的填 NULL。

左圓 ∪ 右圓(聯集)

📌 使用情境:對帳:找出任何一側缺漏的資料

四種 JOIN 實際範例
-- INNER JOIN:只要有訂單的使用者
SELECT u.name, o.product, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

-- LEFT JOIN:所有使用者,沒訂單的也要出現
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
-- 沒有訂單的使用者:order_count = 0(因為 COUNT(NULL) = 0)

-- 多表 JOIN:users + orders + products(假設有 products 表)
SELECT u.name, o.amount, p.category
FROM orders o
JOIN users    u ON o.user_id    = u.id
JOIN products p ON o.product_id = p.id
WHERE o.status = 'shipped';

-- SELF JOIN:在同一張表查關聯(例如員工與主管都在 employees 表)
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

⚠️ 常見陷阱:JOIN 後的 COUNT

LEFT JOIN 後,如果用 COUNT(*) 計算沒有訂單的使用者,會得到 1 而非 0, 因為那一列本身存在(只是 order 欄位都是 NULL)。
正確做法是 COUNT(o.id)——COUNT 遇到 NULL 自動跳過,結果才會是 0。


SQL 的執行順序(面試必考)

你寫 SQL 的順序是 SELECT → FROM → WHERE,但資料庫實際執行的順序完全不同。 理解這個順序,才能寫出正確的查詢(特別是 WHERE vs HAVING 的差異)。

1FROM / JOIN先確定資料來源,執行 JOIN 把表合併
2WHERE過濾原始資料列(不能用 SELECT 定義的別名)
3GROUP BY把資料分組
4HAVING過濾分組後的結果
5SELECT選擇要輸出的欄位、計算聚合函數
6ORDER BY排序(可以用 SELECT 定義的別名)
7LIMIT / OFFSET截取最終結果

本篇重點回顧

🗂️關聯式資料庫用表格組織資料,PK 唯一識別每列,FK 建立表之間的關聯。
🏗️CREATE TABLE 時就定義好約束(NOT NULL / UNIQUE / CHECK),比在應用層驗證更可靠。
✏️CRUD 是所有操作的基礎,UPDATE / DELETE 一定要記得加 WHERE,軟刪除比硬刪除更安全。
🔗JOIN 是 SQL 的核心:INNER 取交集,LEFT 保留左表全部,RIGHT 保留右表全部,FULL OUTER 取聯集。
📊GROUP BY + 聚合函數(COUNT/SUM/AVG)用來統計,HAVING 過濾分組後的結果(不是 WHERE)。
⚙️SQL 執行順序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。

SQLDatabasePostgreSQLSELECTJOINCRUDRelational DatabaseEP.01