SQL JOIN 完全指南:從基礎到實戰

SQL JOIN 完全指南:從基礎到實戰 ​📝 TL;DR(太長不看版) SQL JOIN 是關聯式資料庫中組合多個資料表的關鍵技術。本文將帶你理解 INNER JOIN(交集)、LEFT JOIN(保留左表)、RIGHT JOIN(保留右表)、FULL OUTER JOIN(聯集)的差異,並透過視覺化圖表和實戰案例,讓你輕鬆掌握資料表關聯查詢的技巧。

SQL JOIN 完全指南:把兩張表拼起來用互動投影片走過五種 JOIN、決策樹、ON vs WHERE 經典陷阱與效能排查。

開啟投影片 前置知識 ​在開始之前,建議你先了解以下概念:

基本 SQL 語法 - 能夠撰寫簡單的 SELECT、FROM、WHERE 查詢資料表結構 - 理解主鍵(Primary Key)和外鍵(Foreign Key)的概念關聯式資料庫 - 了解資料表之間的關係(一對一、一對多、多對多)什麼是 SQL JOIN? ​想像你有兩個資料表:一個記錄「學生資訊」,另一個記錄「選課記錄」。如果你想知道「每位學生選了哪些課」,就需要使用 JOIN 來關聯這兩個資料表。

JOIN 就像是把兩張表格橫向拼接起來,根據共同的欄位(如學生 ID)將相關的資料組合在一起。這是關聯式資料庫最強大的功能之一!

JOIN 的視覺化圖解 ​

上圖展示了不同 JOIN 類型如何組合兩個資料表的資料。讓我們用更直觀的方式理解:

JOIN 類型詳解 ​1. INNER JOIN(內連接)- 找交集 ​作用: 只返回兩個資料表中同時符合條件的資料列,就像數學的「交集」。

生活比喻: 想像你有兩份名單:「已註冊的學生」和「已選課的學生」。INNER JOIN 會找出「既註冊又選課」的學生,沒選課的學生不會出現在結果中。

基本語法:

sqlSELECT 欄位列表

FROM 左表

INNER JOIN 右表 ON 左表.關聯欄位 = 右表.關聯欄位;實際範例:

sql-- 查詢已選課的學生資訊

SELECT

s.student_id,

s.name AS student_name,

c.course_name,

c.credits

FROM students AS s

INNER JOIN courses AS c ON s.student_id = c.student_id;查詢結果說明:

✅ 顯示:有選課的學生資訊❌ 不顯示:沒有選課的學生❌ 不顯示:沒有學生選修的課程2. LEFT JOIN(左外連接)- 保留左表全部 ​作用: 返回左表的所有資料,並嘗試匹配右表的資料。如果右表沒有符合的資料,則以 NULL 填充。

生活比喻: 班級老師要統計「所有學生的選課狀況」,即使某些學生還沒選課,也要列在名單上(選課欄位顯示為空)。

基本語法:

sqlSELECT 欄位列表

FROM 左表

LEFT JOIN 右表 ON 左表.關聯欄位 = 右表.關聯欄位;實際範例:

sql-- 列出所有學生,顯示其選課狀況(包含未選課的學生)

SELECT

s.student_id,

s.name AS student_name,

c.course_name,

CASE

WHEN c.course_name IS NULL THEN '未選課'

ELSE '已選課'

END AS enrollment_status

FROM students AS s

LEFT JOIN courses AS c ON s.student_id = c.student_id;查詢結果說明:

✅ 顯示:左表(students)的所有學生✅ 顯示:有選課的學生會匹配到課程資訊✅ 顯示:沒選課的學生,課程欄位顯示 NULL常見應用場景:

找出缺失資料:查詢沒有訂單的客戶sqlSELECT c.customer_name

FROM customers c

LEFT JOIN orders o ON c.customer_id = o.customer_id

WHERE o.order_id IS NULL; -- 沒有訂單的客戶統計關聯數量:計算每位學生的選課數量sqlSELECT

s.name,

COUNT(c.course_id) AS course_count

FROM students s

LEFT JOIN courses c ON s.student_id = c.student_id

GROUP BY s.student_id, s.name;3. RIGHT JOIN(右外連接)- 保留右表全部 ​作用: 與 LEFT JOIN 相反,返回右表的所有資料,並嘗試匹配左表。實務上較少使用,因為可以用 LEFT JOIN 替代。

基本語法:

sqlSELECT 欄位列表

FROM 左表

RIGHT JOIN 右表 ON 左表.關聯欄位 = 右表.關聯欄位;實際範例:

sql-- 列出所有課程,顯示選修的學生(包含無人選修的課程)

SELECT

c.course_name,

s.name AS student_name

FROM students AS s

RIGHT JOIN courses AS c ON s.student_id = c.student_id;💡 實務建議

RIGHT JOIN 和 LEFT JOIN 效果一樣,只是表格位置對調:

sql-- 這兩個查詢結果相同

A LEFT JOIN B -- 保留 A 的全部

B RIGHT JOIN A -- 保留 A 的全部建議統一使用 LEFT JOIN,程式碼更容易閱讀!

4. FULL OUTER JOIN(全外連接)- 保留所有 ​作用: 返回兩個表格的所有資料,無論是否有匹配。沒有匹配的部分以 NULL 填充。

基本語法:

sqlSELECT 欄位列表

FROM 左表

FULL OUTER JOIN 右表 ON 左表.關聯欄位 = 右表.關聯欄位;實際範例:

sql-- 列出所有學生和所有課程的完整配對狀況

SELECT

COALESCE(s.name, '無學生') AS student_name,

COALESCE(c.course_name, '無課程') AS course_name

FROM students AS s

FULL OUTER JOIN courses AS c ON s.student_id = c.student_id;⚠️ MySQL 不支援

MySQL 不支援 FULL OUTER JOIN,可以用 UNION 模擬:

sqlSELECT * FROM students s LEFT JOIN courses c ON s.student_id = c.student_id

UNION

SELECT * FROM students s RIGHT JOIN courses c ON s.student_id = c.student_id;5. CROSS JOIN(交叉連接)- 笛卡爾積 ​作用: 返回兩個表格的所有可能組合(笛卡爾積)。如果左表有 3 筆資料,右表有 4 筆,結果會有 3 × 4 = 12 筆。

基本語法:

sqlSELECT 欄位列表

FROM 左表

CROSS JOIN 右表;實際應用:

sql-- 產生所有「尺寸 × 顏色」的組合

SELECT

s.size,

c.color

FROM sizes AS s

CROSS JOIN colors AS c;JOIN 類型比較表 ​JOIN 類型左表資料右表資料無匹配時MySQLPostgreSQL使用頻率INNER JOIN有匹配的有匹配的不顯示✅✅⭐⭐⭐⭐⭐LEFT JOIN全部有匹配的右表顯示 NULL✅✅⭐⭐⭐⭐⭐RIGHT JOIN有匹配的全部左表顯示 NULL✅✅⭐⭐FULL OUTER全部全部另一表顯示 NULL❌✅⭐⭐CROSS JOIN全部全部產生所有組合✅✅⭐SELF JOIN全部-表格與自己 JOIN✅✅⭐⭐⭐JOIN 選擇決策樹 ​🔥 實戰練習 ​練習資料準備 ​有兩個資料表,一對多關係:users(使用者) 和 user_quotas(使用者配額)

users 資料表

user_quotas 資料表

test.sql2.9 KB練習 1:基礎 JOIN 查詢(簡單)⭐ ​任務: 請列出每位使用者的總配額,並且按照總配額由大到小排序。

要求:

顯示欄位:id、account、total_value(總配額)使用 SUM() 函數計算總配額按總配額降序排列期望輸出:

idaccounttotal_value2user0293user0384user0481user017💡 參考答案sqlSELECT

u.id,

u.account,

SUM(q.value) AS total_value

FROM users u

LEFT JOIN user_quotas q ON u.id = q.user_id

GROUP BY u.id, u.account

ORDER BY total_value DESC;程式碼說明:

使用 LEFT JOIN 確保所有使用者都被列出(即使沒有配額記錄)SUM(q.value) 計算每位使用者的總配額GROUP BY u.id, u.account 依使用者分組ORDER BY total_value DESC 由大到小排序為什麼用 LEFT JOIN 而非 INNER JOIN?

如果有使用者沒有配額記錄,INNER JOIN 會把他們排除LEFT JOIN 能顯示所有使用者,配額總和為 0 或 NULL練習 2:找出特定資料(簡單)⭐ ​任務: 找出沒有任何配額記錄的使用者。

提示:

使用 LEFT JOIN 找出沒有對應配額的使用者利用 WHERE ... IS NULL 篩選條件💡 參考答案sqlSELECT

u.id,

u.account

FROM users u

LEFT JOIN user_quotas q ON u.id = q.user_id

WHERE q.user_id IS NULL;解題技巧:

LEFT JOIN 會保留所有 users,沒有配額的使用者在 user_quotas 欄位會是 NULLWHERE q.user_id IS NULL 篩選出右表沒有匹配的資料練習 3:多表關聯與統計(中等)⭐⭐ ​任務: 假設新增一個 quota_types(配額類型)表,請完成以下複雜查詢:

sql-- 假設有配額類型表

CREATE TABLE quota_types (

type_id INT PRIMARY KEY,

type_name VARCHAR(50),

max_value INT

);

-- user_quotas 新增 type_id 欄位

ALTER TABLE user_quotas ADD COLUMN type_id INT;要求:

列出每位使用者的「每種配額類型」的總和顯示欄位:account、type_name、total_by_type只顯示總配額 > 3 的記錄按使用者和配額類型排序💡 參考答案與解題思路解題思路:

需要關聯三個表:users ← user_quotas ← quota_types先做 JOIN 建立完整資料集按使用者和配額類型分組統計使用 HAVING 過濾總和 > 3 的資料參考程式碼:

sqlSELECT

u.account,

qt.type_name,

SUM(uq.value) AS total_by_type

FROM users u

LEFT JOIN user_quotas uq ON u.id = uq.user_id

LEFT JOIN quota_types qt ON uq.type_id = qt.type_id

GROUP BY u.id, u.account, qt.type_id, qt.type_name

HAVING SUM(uq.value) > 3

ORDER BY u.account, qt.type_name;進階技巧:

使用多個 LEFT JOIN 串聯三個表GROUP BY 需包含 SELECT 中的非聚合欄位HAVING 用於過濾聚合結果(WHERE 無法使用聚合函數)延伸思考:

如何找出「所有配額類型總和 < 5」的使用者?如何顯示「每位使用者最常使用的配額類型」?練習 4:自我挑戰(困難)⭐⭐⭐ ​任務: 列出「配額總和高於平均值」的使用者,並顯示其超出平均值的百分比。

💡 參考答案sqlWITH user_totals AS (

SELECT

u.id,

u.account,

COALESCE(SUM(q.value), 0) AS total_value

FROM users u

LEFT JOIN user_quotas q ON u.id = q.user_id

GROUP BY u.id, u.account

),

avg_total AS (

SELECT AVG(total_value) AS avg_value

FROM user_totals

)

SELECT

ut.account,

ut.total_value,

at.avg_value,

ROUND((ut.total_value - at.avg_value) / at.avg_value * 100, 2) AS exceed_percentage

FROM user_totals ut

CROSS JOIN avg_total at

WHERE ut.total_value > at.avg_value

ORDER BY exceed_percentage DESC;使用的進階技巧:

CTE(Common Table Expression)簡化複雜查詢CROSS JOIN 讓每筆資料都能取得平均值百分比計算公式:(個別值 - 平均值) / 平均值 × 100常見問題 FAQ ​Q1: INNER JOIN 和 WHERE 條件有什麼差別? ​A: 雖然結果可能相同,但意義不同:

sql-- 方法 1:使用 INNER JOIN(推薦)

SELECT * FROM students s

INNER JOIN courses c ON s.student_id = c.student_id;

-- 方法 2:使用 WHERE(舊式寫法)

SELECT * FROM students s, courses c

WHERE s.student_id = c.student_id;建議使用 JOIN 的原因:

✅ 語意更清晰:JOIN 條件與篩選條件分離✅ 支援 OUTER JOIN:WHERE 無法做到 LEFT/RIGHT JOIN✅ 易於維護:複雜查詢時更容易閱讀Q2: 什麼時候用 INNER JOIN,什麼時候用 LEFT JOIN? ​A: 決策依據:

情況使用原因只需要有關聯的資料INNER JOIN效能更好,資料更精確需要顯示所有主表資料LEFT JOIN避免遺漏未關聯的資料需要找出「沒有關聯」的資料LEFT JOIN + IS NULL用於檢查資料完整性查詢訂單及其客戶(都有資料)INNER JOIN訂單一定有客戶查詢客戶及其訂單(可能無單)LEFT JOIN有些客戶可能還沒下單快速判斷法:

問自己:「如果右表沒有匹配資料,我還要顯示左表的資料嗎?」

→ 要 = LEFT JOIN

→ 不要 = INNER JOINQ3: 為什麼我的 JOIN 查詢很慢? ​A: 可能原因與解決方法:

缺少索引sql-- 檢查關聯欄位是否有索引

SHOW INDEX FROM students;

SHOW INDEX FROM courses;

-- 為關聯欄位建立索引

CREATE INDEX idx_student_id ON courses(student_id);笛卡爾積問題sql-- ❌ 忘記寫 ON 條件,產生笛卡爾積

SELECT * FROM students, courses; -- 結果數 = students 數 × courses 數

-- ✅ 正確寫法

SELECT * FROM students s

INNER JOIN courses c ON s.student_id = c.student_id;過多的 JOINsql-- 如果需要 JOIN 超過 5 個表,考慮:

-- 1. 是否能用子查詢減少 JOIN 數量

-- 2. 是否能建立視圖(VIEW)簡化查詢

-- 3. 資料表設計是否需要調整Q4: JOIN 多個表格的順序重要嗎? ​A: 對於結果正確性不重要,但對效能有影響:

sql-- 查詢優化器會自動調整順序,但你可以引導它

SELECT *

FROM orders o -- 100 萬筆

INNER JOIN customers c -- 10 萬筆

ON o.customer_id = c.id

INNER JOIN products p -- 1 千筆

ON o.product_id = p.id

WHERE p.category = 'Electronics'; -- 篩選後剩 100 筆優化建議:

先 JOIN 小表或有強篩選條件的表使用 EXPLAIN 分析查詢計劃考慮建立覆蓋索引(Covering Index)Q5: LEFT JOIN 後使用 WHERE 條件會失效嗎? ​A: 不會失效,但要注意條件位置:

sql-- ❌ 錯誤:WHERE 會過濾掉 NULL,變成 INNER JOIN 效果

SELECT *

FROM students s

LEFT JOIN courses c ON s.student_id = c.student_id

WHERE c.course_name = 'Math'; -- 只顯示有修 Math 的學生

-- ✅ 正確:把條件放在 ON 子句

SELECT *

FROM students s

LEFT JOIN courses c

ON s.student_id = c.student_id

AND c.course_name = 'Math'; -- 顯示所有學生,Math 欄位可能為 NULL

-- ✅ 正確:要找「沒有修課的學生」

SELECT *

FROM students s

LEFT JOIN courses c ON s.student_id = c.student_id

WHERE c.student_id IS NULL; -- 用 IS NULL 檢查關鍵差異:

ON 條件:在 JOIN 階段過濾WHERE 條件:在 JOIN 完成後過濾Q6: 多對多關係如何 JOIN? ​A: 透過中介表(Junction Table):

sql-- 學生選課系統:多對多關係

-- students ← enrollments → courses

-- 查詢每位學生選修的課程

SELECT

s.name AS student_name,

c.course_name,

e.enrollment_date

FROM students s

INNER JOIN enrollments e ON s.student_id = e.student_id

INNER JOIN courses c ON e.course_id = c.course_id

ORDER BY s.name, c.course_name;中介表設計要點:

sqlCREATE TABLE enrollments (

id INT PRIMARY KEY AUTO_INCREMENT,

student_id INT NOT NULL,

course_id INT NOT NULL,

enrollment_date DATE,

grade VARCHAR(2),

-- 複合主鍵或唯一索引

UNIQUE KEY uk_student_course (student_id, course_id),

FOREIGN KEY (student_id) REFERENCES students(id),

FOREIGN KEY (course_id) REFERENCES courses(id)

);最佳實踐 ​✅ 推薦做法 ​使用表格別名sql-- ✅ 清晰易讀

SELECT s.name, c.course_name

FROM students AS s

INNER JOIN courses AS c ON s.student_id = c.student_id;

-- ❌ 冗長難讀

SELECT students.name, courses.course_name

FROM students

INNER JOIN courses ON students.student_id = courses.student_id;明確指定欄位sql-- ✅ 只選擇需要的欄位

SELECT s.id, s.name, c.course_name

FROM students s

INNER JOIN courses c ON s.student_id = c.student_id;

-- ❌ 避免使用 SELECT *

SELECT * -- 可能包含重複的欄位,浪費資源

FROM students s

INNER JOIN courses c ON s.student_id = c.student_id;為關聯欄位建立索引sqlCREATE INDEX idx_courses_student_id ON courses(student_id);

CREATE INDEX idx_orders_customer_id ON orders(customer_id);❌ 常見錯誤 ​忘記寫 ON 條件sql-- ❌ 產生笛卡爾積

SELECT * FROM students s

INNER JOIN courses c; -- 缺少 ON 條件

-- ✅ 正確寫法

SELECT * FROM students s

INNER JOIN courses c ON s.student_id = c.student_id;混淆 JOIN 類型sql-- 需求:列出所有學生,包含未選課的

-- ❌ 錯誤:只顯示有選課的學生

SELECT * FROM students s

INNER JOIN courses c ON s.student_id = c.student_id;

-- ✅ 正確:使用 LEFT JOIN

SELECT * FROM students s

LEFT JOIN courses c ON s.student_id = c.student_id;在 ON 和 WHERE 中混淆條件sql-- 需求:列出所有學生,標註是否有修 Math

-- ❌ 錯誤:只顯示有修 Math 的學生

SELECT * FROM students s

LEFT JOIN courses c ON s.student_id = c.student_id

WHERE c.course_name = 'Math';

-- ✅ 正確:Math 條件放在 ON

SELECT * FROM students s

LEFT JOIN courses c

ON s.student_id = c.student_id

AND c.course_name = 'Math';延伸閱讀 ​推薦資源 ​外部優質資源:

MySQL 8.0 JOIN 官方文件 - 官方文件,詳細規格說明PostgreSQL JOIN 文件 - PostgreSQL 的 JOIN 教學SQL JOIN 視覺化工具 - 互動式學習 JOIN 類型📝 總結 ​SQL JOIN 是資料庫查詢的核心技能,讓我們回顧關鍵重點:

INNER JOIN - 只取交集,效能最好,適合需要完整資料的場景LEFT JOIN - 保留左表全部,最常用,適合「主表 + 補充資料」的查詢RIGHT JOIN - 保留右表全部,較少使用,可用 LEFT JOIN 替代FULL OUTER JOIN - 保留所有資料,用於資料整合分析索引是關鍵 - 為關聯欄位建立索引能大幅提升效能注意條件位置 - ON 和 WHERE 的差異會影響 LEFT JOIN 結果