前幾篇我們學了 JOIN 的基本用法,都是兩張表合併在一起。
但實務上,你經常需要把三張、四張、甚至更多張表合併在一起。
這篇來看怎麼把多張表串成一條合併鏈,並且用一個完整的實戰練習來演練。
練習:找出沒有人會 TypeScript 的 Web 組別
老闆說:「我們有好幾個 Web 領域的組別,但我擔心有些組裡面沒有人會 TypeScript。幫我把這些組找出來。」
換句話說,老闆要的是同時符合兩個條件的組別:
- 領域是 Web
- 組裡面沒有任何一個組員會 TypeScript
這次的情境是一個工程團隊的管理系統,總共有四張表:
組別表:
每個組別有自己的名稱和所屬領域(Web、機器學習、App 等)。
組員表:
一個組可以有很多組員,但一個組員只能隸屬於一個組別。
所以組員表上直接放了組別ID(Foreign Key),指向組別表。
技術表:
人員技術表(關聯表):
一個組員可以會很多技術,一個技術也可以被很多組員擁有。
這是多對多的關係,所以需要這張關聯表來記錄「誰會什麼技術」。
分析老闆的需求
回到老闆的問題:找出領域是 Web,而且沒有任何組員會 TypeScript 的組別。
要回答這個問題,你的腦袋會自然做這樣的推理:
- 先看哪些組別的領域是 Web → 需要查組別表
- 這些組別裡面有哪些組員 → 需要查組員表
- 這些組員分別會什麼技術 → 需要查人員技術表和技術表
- 有沒有人會 TypeScript?如果沒有,這個組別就是答案
每一步都需要不同的表,所以四張表必須全部串在一起。
但四張表要怎麼串?一步一步來。
解題思路:先做合併,把資料組齊
看到這種需要多張表的問題,不要急著想怎麼篩選。
先把資料組齊再說。
為什麼?因為如果你的資料不齊全,後面的篩選和計算都做不了。
我們最後要挑出的是組別,所以把組別放在最左邊。
這樣不管後面怎麼合併,所有組別都會被保留下來(還記得上一篇的 LEFT JOIN 嗎?左邊的資料全部保留)。
然後從組別出發,一路 LEFT JOIN,把其他表的資料一張一張接上來。
但要從哪張表開始接?我們一步一步來看。
用 LEFT JOIN 串接四張表:合併鏈逐步拆解
第一個 LEFT JOIN:組別 + 組員
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID組別跟誰有直接關係?只有組員表。
因為組員表上有組別ID,所以可以直接合併。
合併結果:
現在我們知道每個組別裡面有哪些組員了。
第二個 LEFT JOIN:加上人員技術表
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID下一步,我們需要知道這些組員會哪些技術。
但組員和技術是多對多的關係,不能直接合併。
必須先經過中間的關聯表(人員技術表),才能走到技術表。
合併結果:
現在我們知道每個組員對應到哪些技術ID了。
但技術ID 只是一個數字,還看不出是什麼技術。
第三個 LEFT JOIN:加上技術表
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID最後一步,把技術的名稱也補上來:
四張表全部串在一起了。
把三個步驟合在一起,就是完整的合併鏈:
SELECT *
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID從組別出發,一路 LEFT JOIN,把需要的資料一張一張接上來。
用 WHERE 篩選出會 TypeScript 的資料
現在資料齊全了,可以開始篩選。
我們的目標是找出「沒有人會 TypeScript 的 Web 組別」。
反過來想:先找出「有人會 TypeScript 的組別」,再把它們排除掉。
先篩選技術名稱是 TypeScript 的資料:
SELECT *
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 技術.名稱 = 'TypeScript'篩選結果:
只剩下有人會 TypeScript 的組別了。
用 GROUP BY 找出有 TypeScript 的組別ID
接下來用 GROUP BY 分組,找出哪些組別有人會 TypeScript:
SELECT 組別.組別ID
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 技術.名稱 = 'TypeScript'
GROUP BY 組別.組別ID結果:
這就是「有人會 TypeScript」的組別ID 清單。
用 NOT IN 排除組別,找出最終答案
最後,從組別表中排除這些ID,再加上領域是 Web 的條件:
SELECT *
FROM 組別
WHERE 組別ID NOT IN (
SELECT 組別.組別ID
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 技術.名稱 = 'TypeScript'
GROUP BY 組別.組別ID
)
AND 領域 = 'Web'內層的子查詢找出「有人會 TypeScript 的組別ID」。
外層用 NOT IN 把這些ID 排除掉。
原本的組別表長這樣:
排掉組別ID 1 之後:
再加上 AND 領域 = 'Web',就只剩下「Web 領域中沒有人會 TypeScript 的組別」。
結果:
前端二組的 Carol 只會 React,不會 TypeScript,所以被挑了出來。
延伸練習:用 LEFT JOIN 合併子查詢,算出 TypeScript 人數
老闆又問了:「我不只想知道哪些組沒人會 TypeScript,我還想看每個 Web 組別裡面,會 TypeScript 的人到底有幾個。」
換句話說,老闆要的是一張表:
- 列出所有 Web 領域的組別
- 每個組別旁邊顯示「會 TypeScript 的人數」
- 如果沒人會,顯示 0
這題的關鍵在於:每個組別都要出現在結果裡,包括沒人會 TypeScript 的組別。
換一個思路:先用子查詢算出一張「統計表」,裡面記錄每個組別有幾個人會 TypeScript。
但這張統計表有個問題:沒有人會 TypeScript 的組別根本不會出現在裡面。
所以第二步,把組別表放在左邊,用 LEFT JOIN 合併這張統計表。
因為是 LEFT JOIN,組別表的每一筆都會保留。
如果某個組別在統計表裡找不到對應,人數那欄就會是 NULL。
最後用 COALESCE 把 NULL 補成 0,就能看到完整的結果。
先算出每個組別有幾個人會 TypeScript
SELECT 組別.組別ID, COUNT(*) AS 人數
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 技術.名稱 = 'TypeScript'
GROUP BY 組別.組別ID結果:
怎麼只有一筆?其他組別去哪了?
回想一下合併鏈跑出來的資料:
加上 WHERE 技術.名稱 = 'TypeScript' 之後,只有技術名稱是 TypeScript 的那一筆會留下來:
前端二組、AI 團隊、App 團隊的技術都不是 TypeScript,整筆被 WHERE 篩掉了。
所以 GROUP BY 之後,自然也只剩下組別ID 1。
這就是前面說的「統計表的問題」:沒有人會 TypeScript 的組別,根本不會出現在這張表裡。
用 LEFT JOIN 合併回組別表
現在要把組別表和剛才的統計表合併在一起。
概念上就是:組別表放在左邊,LEFT JOIN 子查詢的結果。
FROM 組別
LEFT JOIN (
子查詢
)帶入子查詢
把剛才的子查詢帶進去:
FROM 組別
LEFT JOIN (
SELECT 組別.組別ID, COUNT(*) AS 人數
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 技術.名稱 = 'TypeScript'
GROUP BY 組別.組別ID
) ??? ON ???子查詢帶進去了,但後面的 ON 還沒辦法寫,因為我們還沒幫子查詢取名字。
幫子查詢取別名
子查詢放在 FROM 後面的時候,SQL 規定一定要給它一個別名,這裡取名為 ts:
FROM 組別
LEFT JOIN (
SELECT 組別.組別ID, COUNT(*) AS 人數
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 技術.名稱 = 'TypeScript'
GROUP BY 組別.組別ID
) AS ts ON 組別.組別ID = ts.組別ID有了別名 ts 之後,就可以用 ts.組別ID、ts.人數 來引用子查詢的欄位,ON 也能寫了。
因為是 LEFT JOIN,組別表的每一筆都會保留。
如果某個組別在子查詢的結果裡找不到對應(代表沒人會 TypeScript),人數那欄就會是 NULL。
像這樣:
只有前端一組在統計表裡有對應,其他三個組別的人數都是 NULL。
加上 WHERE 篩選 Web 領域
再加上 WHERE 組別.領域 = 'Web',只留下 Web 領域的組別:
FROM 組別
LEFT JOIN (
SELECT 組別.組別ID, COUNT(*) AS 人數
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 技術.名稱 = 'TypeScript'
GROUP BY 組別.組別ID
) AS ts ON 組別.組別ID = ts.組別ID
WHERE 組別.領域 = 'Web'AI 團隊和 App 團隊被篩掉了,只剩下兩個 Web 組別:
前端二組的人數還是 NULL,接下來用 COALESCE 把它補成 0。
用 COALESCE 把 NULL 補成 0
用 COALESCE(ts.人數, 0) 把 NULL 補成 0,寫出完整的 SELECT:
SELECT
組別.組別ID,
組別.名稱,
COALESCE(ts.人數, 0) AS TypeScript人數
FROM 組別
LEFT JOIN (
SELECT 組別.組別ID, COUNT(*) AS 人數
FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 技術.名稱 = 'TypeScript'
GROUP BY 組別.組別ID
) AS ts ON 組別.組別ID = ts.組別ID
WHERE 組別.領域 = 'Web'結果:
前端二組在子查詢裡找不到對應,人數原本是 NULL,被 COALESCE 補成 0。
另外,你可能注意到前一步還有的「領域」欄位,在最終結果裡消失了。
這是因為 SELECT 只選了組別ID、名稱、TypeScript人數這三個欄位。WHERE 可以用領域來篩選,但不代表它一定要出現在結果裡。
小結
這篇學到了兩個重要的觀念:
合併鏈:
JOIN 不是只能合併兩張表。
你可以從一張主要的表出發,一路 LEFT JOIN 下去,把需要的輔助表一張一張接上來。
遇到多對多的關係,必須先經過關聯表,才能走到目標表。
解題的思路:
不知道怎麼開始的時候,先做合併。
先用 SELECT * 把所有資料組出來,確認輸入正確,再去想篩選和計算。
輸入最重要,如果資料不齊全,後面的計算都做不了。