JOIN 是多表關聯查詢的核心。本文系統講解 MySQL 五種 JOIN 類型語法、視覺化集合關係、執行機制、以及 7 大性能優化實戰技巧。
SELECT select_list
FROM table1
[INNER | LEFT | RIGHT] JOIN table2 ON join_condition
[WHERE where_condition];
MySQL 不支援 FULL JOIN,可用 LEFT JOIN UNION RIGHT JOIN 模擬。
| 類型 | 關鍵字 | 結果集 | 集合圖示 |
|---|---|---|---|
| 內連接 | INNER JOIN | A ∩ B (雙方匹配行) | 🔵🔵 重疊區域 |
| 左連接 | LEFT [OUTER] JOIN | A 全集 + B 匹配 (無匹配填 NULL) | 🔵⚪ 左圓完整 |
| 右連接 | RIGHT [OUTER] JOIN | B 全集 + A 匹配 (無匹配填 NULL) | ⚪🔵 右圓完整 |
| 交叉連接 | CROSS JOIN | A × B (笛卡爾積,全排列) | 🔵🔵 矩形網格 |
| 全連接 | FULL JOIN (模擬) | A ∪ B (雙方全集,無匹配填 NULL) | 🔵🔵 兩圓完整 |
-- 表 A
id | name
1 | Pirate
2 | Monkey
3 | Ninja
4 | Spaghetti
-- 表 B
id | name
1 | Rutabaga
2 | Pirate
3 | Darth Vader
4 | Ninja
-- INNER JOIN
SELECT * FROM A INNER JOIN B ON A.name = B.name;
-- 1 Pirate | 2 Pirate
-- 3 Ninja | 4 Ninja
-- LEFT JOIN
SELECT * FROM A LEFT JOIN B ON A.name = B.name;
-- 1 Pirate | 2 Pirate
-- 2 Monkey | NULL NULL
-- 3 Ninja | 4 Ninja
-- 4 Spaghetti| NULL NULL
-- RIGHT JOIN
SELECT * FROM A RIGHT JOIN B ON A.name = B.name;
-- NULL NULL | 1 Rutabaga
-- 1 Pirate | 2 Pirate
-- NULL NULL | 3 Darth Vader
-- 3 Ninja | 4 Ninja
-- CROSS JOIN (笛卡爾積 4×4=16 行)
SELECT * FROM A CROSS JOIN B;
-- FULL JOIN 模擬
SELECT * FROM A LEFT JOIN B ON A.name = B.name
UNION
SELECT * FROM A RIGHT JOIN B ON A.name = B.name;
-- 僅 A 有、B 無 (差集)
SELECT * FROM A LEFT JOIN B ON A.name = B.name WHERE B.id IS NULL;
-- 對稱差集 (A有B無 + B有A無)
SELECT * FROM A LEFT JOIN B ON A.name = B.name WHERE B.id IS NULL
UNION
SELECT * FROM A RIGHT JOIN B ON A.name = B.name WHERE A.id IS NULL;
-- 以下三者在 MySQL 中等價 (無 ON 時均為笛卡爾積)
SELECT * FROM A CROSS JOIN B;
SELECT * FROM A INNER JOIN B;
SELECT * FROM A JOIN B;
-- 加上 ON 條件後,CROSS JOIN 退化為 INNER JOIN
SELECT * FROM A CROSS JOIN B ON A.name = B.name; -- 等同 INNER JOIN
-- 顯式 (推薦)
SELECT * FROM a INNER JOIN b ON a.id = b.id;
-- 隱式 (舊語法)
SELECT a.*, b.* FROM a, b WHERE a.id = b.id;
INNER JOIN 可由優化器自由調整驅動表順序;LEFT JOIN 強制左表為驅動表,限制優化空間。
-- 邏輯不同!
-- ON: 決定如何從右表匹配行,無匹配則生成 NULL 行
-- WHERE: 在匹配完成後過濾結果行
-- 範例:查詢商品及其最新價格 (若無價格仍顯示商品)
SELECT p.*, pr.price
FROM product p
LEFT JOIN price pr ON pr.product_id = p.id AND pr.is_latest = 1 -- 正確:ON 中過濾
-- 錯誤寫法:
LEFT JOIN price pr ON pr.product_id = p.id
WHERE pr.is_latest = 1 -- 將 LEFT JOIN 退化為 INNER JOIN,無價格商品被過濾掉
-- 一般寫法
SELECT * FROM A
INNER JOIN B ON B.name = A.name
LEFT JOIN C ON C.name = B.name
LEFT JOIN D ON D.id = C.id
WHERE C.status > 1 AND D.status = 1;
-- 優化:條件下推 ON
SELECT * FROM A
INNER JOIN B ON B.name = A.name
LEFT JOIN C ON C.name = B.name AND C.status > 1
LEFT JOIN D ON D.id = C.id AND D.status = 1;
-- 子查詢 (效能差)
INSERT INTO t1(a1)
SELECT b1 FROM t2
WHERE NOT EXISTS (SELECT 1 FROM t1 WHERE t1.id = t2.r_id);
-- JOIN 改寫 (效能好)
INSERT INTO t1(a1)
SELECT b1 FROM t2
LEFT JOIN (SELECT DISTINCT id FROM t1) t1 ON t1.id = t2.r_id
WHERE t1.id IS NULL;
EXPLAIN 確認 type=ref、key=索引名SELECT /*+ MAX_EXECUTION_TIME(1000) */ STRAIGHT_JOIN
small_table.*, big_table.*
FROM small_table
INNER JOIN big_table ON small_table.id = big_table.small_id;
| 欄位 | 優秀值 | 說明 |
|---|---|---|
| type | ref, eq_ref | 索引查找,非 ALL/index |
| key | 實際用到的索引 | NULL 表示無索引可用 |
| rows | 較小 | 預估掃描行數 |
| Extra | Using index, Using where | Using temporary/filesort 需優化 |