本日主題:把多張表的資料關聯起來查詢 + VIEW 觀念 預計時間:2 小時 對應主教材:第 6.1 節
一、今日學習目標
- [ ] 理解為什麼要 JOIN
- [ ] 會寫 INNER JOIN 與 LEFT JOIN
- [ ] 理解各種 JOIN 類型的差別
- [ ] 認識 VIEW(檢視表)的觀念
- [ ] 會寫 JOIN + 子查詢的組合
二、教材內容
2.1 為什麼要 JOIN?
資料庫為了避免重複,會把資料拆成多張表(正規化)。例如 employees 只存 dept_id,部門名稱放在 departments。要同時看「員工名 + 部門名」,就要把兩張表關聯(JOIN)起來。
employees departments
id name dept_id id dept_name
1 王小明 1 ───┐ 1 IT
2 李大華 1 ───┤ 2 會計
3 陳美麗 2 ───┘──▶ 3 維修
4 林志強 3 ───────▶
5 張雅婷 1
6 黃建宏 3
透過 dept_id 和 departments.id 建立關聯
2.2 INNER JOIN(最常用)
只回傳「兩邊都對得上」的資料:
SELECT e.name, d.dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.id;
e、d是表的別名(alias),讓 SQL 更簡潔。ON指定關聯條件(哪兩個欄位對應)。JOIN等同於INNER JOIN。
INNER JOIN 圖解:
┌─────────┐ ┌───────────┐
│employees │ │departments │
│ │ │ │
│ ┌─────┼───┼─────┐ │
│ │ 交集 │ │ │ │
│ │ │ │ │ │
│ └─────┼───┼─────┘ │
│ │ │ │
└─────────┘ └───────────┘
只回傳交集(兩邊都對得上的資料)
2.3 LEFT JOIN
保留「左表」全部資料,右表沒對到的補 NULL:
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id;
- 用途:想看「所有員工,即使部門資料缺失也要列出」。
LEFT JOIN 圖解:
┌─────────┐ ┌───────────┐
│employees │ │departments │
│ ████████ │ │ │
│ ████┌────┼───┼─────┐ │
│ ████│交集 │ │ │ │
│ ████│ │ │ │ │
│ ████└────┼───┼─────┘ │
│ ████████ │ │ │
└─────────┘ └───────────┘
保留左表全部 + 交集部分
右表沒對到的欄位補 NULL
2.4 JOIN 類型對照
| 類型 | 回傳 | 使用場景 |
|---|---|---|
| INNER JOIN | 兩邊都有對到的 | 最常用,只要匹配的資料 |
| LEFT JOIN | 左表全部 + 右表對到的 | 要看左表全部,即使沒有匹配 |
| RIGHT JOIN | 右表全部 + 左表對到的 | 較少用,可用 LEFT JOIN 互換 |
| FULL JOIN | 兩邊全部 | 要看所有資料(部分資料庫不支援) |
| CROSS JOIN | 笛卡爾積(所有組合) | 很少用,產生大量資料 |
2.5 JOIN 常見錯誤
-- ❌ 錯誤 1:忘記 ON 條件
SELECT e.name, d.dept_name
FROM employees e
JOIN departments d; -- 沒有 ON → 變成 CROSS JOIN(笛卡爾積)
-- ❌ 錯誤 2:ON 條件寫錯欄位
SELECT e.name, d.dept_name
FROM employees e
JOIN departments d ON e.id = d.id; -- 應該用 e.dept_id = d.id
-- ❌ 錯誤 3:欄位名稱衝突沒加表別名
SELECT id, name, dept_name -- id 在兩張表都有,會報錯
FROM employees e
JOIN departments d ON e.dept_id = d.id;
-- 正確:SELECT e.id, e.name, d.dept_name
2.6 JOIN + 分組(綜合)
-- 每個部門的人數(用部門名顯示)
SELECT d.dept_name, COUNT(e.id) AS 人數
FROM departments d
LEFT JOIN employees e ON e.dept_id = d.id
GROUP BY d.dept_name;
-- 每個部門的平均薪水(只顯示有人的部門)
SELECT d.dept_name, AVG(e.salary) AS 平均薪水
FROM departments d
JOIN employees e ON e.dept_id = d.id
GROUP BY d.dept_name;
2.7 JOIN + 子查詢
-- 找出薪水高於其部門平均的員工
SELECT e.name, e.salary, e.department, dept_avg.avg_sal
FROM employees e
JOIN (
SELECT department, AVG(salary) AS avg_sal
FROM employees
GROUP BY department
) AS dept_avg ON e.department = dept_avg.department
WHERE e.salary > dept_avg.avg_sal;
2.8 VIEW(檢視表)觀念
VIEW 是一個「儲存的查詢」,像是虛擬的表格,不實際儲存資料:
-- 建立 VIEW
CREATE VIEW employee_detail AS
SELECT e.name, e.salary, d.dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.id;
-- 使用 VIEW(跟查詢一般表一樣)
SELECT * FROM employee_detail;
SELECT * FROM employee_detail WHERE dept_name = 'IT';
-- 刪除 VIEW
DROP VIEW employee_detail;
VIEW 的優點:
┌──────────────────────────────────────────────┐
│ VIEW 的好處 │
├──────────────────────────────────────────────┤
│ │
│ ✓ 簡化複雜查詢:把常用的 JOIN 查詢存成 VIEW │
│ ✓ 安全性:只讓使用者看到特定欄位 │
│ (如 VIEW 不含薪水欄位) │
│ ✓ 一致性:所有人用同一個 VIEW,避免寫錯 │
│ ✓ 維護性:底層表改了,只需修改 VIEW 定義 │
│ │
│ 注意:VIEW 不儲存資料,每次查詢都會執行底層 SQL │
└──────────────────────────────────────────────┘
常見錯誤示範:
-- ❌ 錯誤:對 VIEW 做 INSERT(多數 VIEW 不支援寫入)
INSERT INTO employee_detail VALUES ('新員工', 50000, 'IT');
-- VIEW 是虛擬表,通常只能讀取
-- ❌ 錯誤:以為 VIEW 會自動更新
-- VIEW 不儲存資料,底層表的資料變了,VIEW 的結果會自動反映
-- 但 VIEW 的「定義」不會自動改變
三、常見誤解
| 誤解 | 正確觀念 |
|---|---|
| 「JOIN 會把兩張表合併成一張」 | JOIN 是查詢時關聯,不會改變原表 |
| 「LEFT JOIN 和 RIGHT JOIN 效果一樣」 | 不同,取決於哪張表在左邊。LEFT JOIN 保留左表全部 |
| 「VIEW 會佔用儲存空間」 | VIEW 不儲存資料,只儲存查詢定義。Materialized View(物化檢視)才會 |
| 「JOIN 越多越好」 | JOIN 太多會影響效能,應只 JOIN 需要的表 |
四、面試加分小知識
- Self JOIN:同一張表自己 JOIN 自己,用於「主管-部屬」等層級關係。
- Materialized View(物化檢視):與 VIEW 不同,會實際儲存資料,需定期更新。
- N+1 Problem:ORM 中常見的效能問題,沒用好 JOIN 導致多次查詢。
- Cartesian Product(笛卡爾積):CROSS JOIN 或忘記 ON 條件時產生,列數 = 左表 × 右表。
五、今日練習
Q1. 查出每位員工的姓名與其部門名稱(用 dept_id 關聯)。
Q2. INNER JOIN 和 LEFT JOIN 的差別?
Q3. 用 JOIN + GROUP BY 算出每個部門的平均薪水(顯示部門名稱)。
Q4. 用 LEFT JOIN 找出「沒有任何員工的部門」。
Q5. 建立一個 VIEW,顯示員工姓名、薪水、部門名稱。然後用這個 VIEW 查詢 IT 部門的資料。
參考解答
-- A1:員工姓名 + 部門名稱
SELECT e.name, d.dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.id;
-- A2:
-- INNER JOIN 只回傳兩邊都對得上的列
-- LEFT JOIN 保留左表全部,右表沒對到的補 NULL
-- 例如:有員工 dept_id=99 但 departments 沒有 id=99
-- INNER JOIN 不會顯示這個員工
-- LEFT JOIN 會顯示這個員工,dept_name 為 NULL
-- A3:每部門平均薪水(用部門名稱)
SELECT d.dept_name, AVG(e.salary) AS 平均薪水
FROM departments d
JOIN employees e ON e.dept_id = d.id
GROUP BY d.dept_name;
-- A4:沒有員工的部門
SELECT d.dept_name
FROM departments d
LEFT JOIN employees e ON e.dept_id = d.id
WHERE e.id IS NULL;
-- 說明:LEFT JOIN 後,沒有員工的部門,e.id 會是 NULL
-- A5:建立 VIEW 並查詢
CREATE VIEW employee_detail AS
SELECT e.name AS 姓名, e.salary AS 薪水, d.dept_name AS 部門
FROM employees e
JOIN departments d ON e.dept_id = d.id;
SELECT * FROM employee_detail WHERE 部門 = 'IT';
六、延伸閱讀
- Visual Representation of SQL Joins — JOIN 圖解(經典文章)
- SQL VIEW (W3Schools) — VIEW 教學
七、今日檢核
- [ ] 我理解為什麼需要 JOIN
- [ ] 我會寫 INNER JOIN 與 LEFT JOIN
- [ ] 我能說出各種 JOIN 類型的差別
- [ ] 我會用 JOIN + GROUP BY 做統計
- [ ] 我理解 VIEW 的觀念與用法
⬅️ 上一天:Day 3 | 🏠 本週總覽 | ➡️ 下一天:Day 5 — 新增/更新/刪除