前面學的 JOIN 是「水平合併」——把不同表的欄位橫向接起來,讓一列資料變得更寬。
但有些時候,我們需要的不是把欄位橫向接起來,而是把一張表的資料列,接在另一張表的下面。
例如 A 表查出 3 筆結果:
B 表查出 2 筆結果:
我想把這 5 筆放在同一張表裡:
就是把 B 表的資料列,直接接在 A 表的下面。
這種操作叫做垂直整併。
垂直整併比 JOIN 簡單,因為不需要設合併條件——你就是把兩組資料接在一起而已。
SQL 提供了三種垂直整併的方式:聯集(UNION)、交集(INTERSECT)、差集(EXCEPT)。
先來看為什麼需要垂直整併
老闆說:「我們現在要全面發展 Web 方向,機器學習先暫緩。」
「幫我列出所有跟 Web 有關的組員。有兩種情況:一種是組別領域是 Web 的,另一種是技術領域有 Web 的。兩種都幫我挑出來,放在一張清單裡。」
沿用前幾篇的四張表:
組別表:
組員表:
技術表:
人員技術表:
策略一:全部 JOIN 在一起再篩選
第一個想到的做法:把四張表全部 JOIN 在一起,變成一張大表,再用 WHERE 篩選。
SELECT 組員.組員ID, 組員.名稱
FROM 組員
LEFT JOIN 組別 ON 組員.組別ID = 組別.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 組別.領域 = 'Web' OR 技術.領域 = 'Web'四張表 JOIN 完之後,會產生一張大表(共 7 筆):
Frank 因為沒有任何技術紀錄,LEFT JOIN 之後技術名稱和技術領域都是 NULL。
再用 WHERE 組別.領域 = 'Web' OR 技術.領域 = 'Web' 篩選,Dave 和 Eve 被排掉,Frank 因為組別領域是 Web 所以留下來:
結果是對的,但有一個問題:效能不好。
回頭看一下老闆的需求,其實是兩件獨立的事:
- 組別領域是 Web 的組員
- 技術領域有 Web 的組員
這兩件事分別只需要兩張表就能查出來:第一件查組員和組別,第二件查組員、人員技術和技術。
但我們為了在同一個 WHERE 裡同時判斷 組別.領域 = 'Web' OR 技術.領域 = 'Web',只好把四張表全部 JOIN 在一起。
這樣做有兩個代價。
第一,JOIN 的次數變多了。
四張表要做三次 JOIN,每次 JOIN 都會產生排列組合。
表越多,排列組合越大,資料庫要處理的資料量也越大。
第二,結果會出現重複。
你看上面的大表,Alice 出現了兩次,因為她會兩種技術(TypeScript 和 React),每種技術都產生一筆配對。
但老闆只是要知道「Alice 跟 Web 有關」,不需要看到她出現兩次。
這些重複和多餘的 JOIN,都是因為我們硬把四張表塞進同一個查詢造成的。
策略二:分開查,再接在一起
換一個思路:兩件事分開查,各自得到一張小表,再把兩張小表接在一起。
這就是垂直整併。
SQL 提供了三種垂直整併的方式:聯集(UNION)、交集(INTERSECT)、差集(EXCEPT)。
我們逐一來看,每一種都會搭配實際的例子。
聯集(UNION):兩邊都要
UNION 會把上面的查詢結果和下面的查詢結果接在一起。
如果有重複的資料列,預設只留一筆(自動去除重複)。
如果要保留重複,用 UNION ALL。
回到老闆的需求:把組別領域是 Web 的組員,和技術領域有 Web 的組員,放在同一張清單裡。
兩邊都要,而且重複的人只列一次,剛好符合 UNION 的行為。
SELECT 組員.組員ID, 組員.名稱
FROM 組員
LEFT JOIN 組別 ON 組員.組別ID = 組別.組別ID
WHERE 組別.領域 = 'Web'
UNION
SELECT 組員.組員ID, 組員.名稱
FROM 組員
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 技術.領域 = 'Web'UNION 上面和下面的查詢各自獨立執行。
上面的查詢結果(組別領域是 Web 的組員):
下面的查詢結果(技術領域有 Web 的組員):
Frank 不在下面的結果裡,因為他沒有任何技術紀錄,自然也沒有 Web 技術。
UNION 接在一起並去除重複後:
Frank 雖然只出現在上面的結果,UNION 還是會把他收進來。
這正是老闆要的:符合任一條件就列出來。
跟策略一(四張表全部 JOIN)比起來,策略二只做了兩次小規模的 JOIN,不需要把四張表全部合併成一張大表。
欄位必須一致
使用 UNION 有一個重要規則:上下兩組查詢的欄位數量、名稱和型別必須一致。
因為垂直整併是把資料列接在一起,如果上面是兩個欄位,下面是三個欄位,根本接不起來。
而且在判斷「這兩筆資料是不是重複」的時候,資料庫會逐欄比對。
如果欄位名稱不一樣,資料庫就無法判斷哪些是重複的。
上面的例子中,兩組查詢都 SELECT 了 組員.組員ID 和 組員.名稱,欄位完全一致,所以可以正確地接在一起。
這個規則對後面要介紹的 INTERSECT 和 EXCEPT 也一樣適用。
交集(INTERSECT):只要重疊的
第二種是交集(INTERSECT)。
INTERSECT 只留下上面和下面的查詢結果中都有的資料列。
預設去除重複,加上 ALL 保留重複。
INTERSECT 不分左右:A INTERSECT B 跟 B INTERSECT A 的結果一樣。
同樣的需求,換成 INTERSECT 會怎樣?
回到老闆的需求:「兩種都幫我挑出來,放在一張清單裡。」
剛才用 UNION 解決了。
如果把同樣的查詢換成 INTERSECT,會怎樣?
SELECT 組員.組員ID, 組員.名稱
FROM 組員
LEFT JOIN 組別 ON 組員.組別ID = 組別.組別ID
WHERE 組別.領域 = 'Web'
INTERSECT
SELECT 組員.組員ID, 組員.名稱
FROM 組員
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 技術.領域 = 'Web'上面的查詢結果(組別領域是 Web 的組員):
下面的查詢結果(技術領域有 Web 的組員):
INTERSECT 只留下兩邊都有的:
Frank 出現在上面的結果(組別領域是 Web),但沒有出現在下面的結果(他沒有 Web 技術)。
INTERSECT 要求兩邊都有才留下,所以 Frank 被排掉了。
但老闆說的是「兩種都幫我挑出來」,意思是符合任一條件就要列出來。
Frank 的組別領域是 Web,他應該要出現在清單裡。
INTERSECT 卻只留下同時符合兩個條件的人,範圍變小了,不符合需求。
所以這個情境適合用 UNION,不適合用 INTERSECT。
INTERSECT 適合的場景是:你有兩組條件,而且你只想要同時滿足兩個條件的資料。
差集(EXCEPT):從 A 扣掉 B
第三種是差集(EXCEPT)。
EXCEPT 會從上面的查詢結果中,把下面的查詢結果也有的資料列移除,只留下上面獨有的。
預設去除重複,加上 ALL 保留重複。
差集跟前面兩種不同,它有方向性:A EXCEPT B 是「從 A 扣掉 B」,B EXCEPT A 是「從 B 扣掉 A」,結果不一樣。
聯集和交集則不分左右:A UNION B 跟 B UNION A 結果一樣,A INTERSECT B 跟 B INTERSECT A 也一樣。
同樣的需求,換成 EXCEPT 會怎樣?
回到老闆的需求:「兩種都幫我挑出來,放在一張清單裡。」
如果把同樣的查詢換成 EXCEPT,會怎樣?
SELECT 組員.組員ID, 組員.名稱
FROM 組員
LEFT JOIN 組別 ON 組員.組別ID = 組別.組別ID
WHERE 組別.領域 = 'Web'
EXCEPT
SELECT 組員.組員ID, 組員.名稱
FROM 組員
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID
WHERE 技術.領域 = 'Web'上面的查詢結果(組別領域是 Web 的組員):
下面的查詢結果(技術領域有 Web 的組員):
EXCEPT 從上面的結果扣掉下面也有的,只留下上面獨有的:
Alice、Bob、Carol 同時出現在上下兩邊,被 EXCEPT 扣掉了,只剩下 Frank。
但老闆要的是所有跟 Web 有關的人,不是「只在組別領域是 Web、但沒有 Web 技術」的人。
EXCEPT 的結果只有 Frank 一個人,少了 Alice、Bob、Carol,完全不符合需求。
所以這個情境也不適合用 EXCEPT。
EXCEPT 適合的場景是:你想從 A 裡面排掉 B 已經有的,只留下 A 獨有的部分。
用 EXCEPT 解決 <> 排不掉的問題
前一篇文章提到,用 <> 找「組員還不會的技術」會失敗,因為多對多的關係下 <> 排不乾淨。
現在用 EXCEPT 就能解決這個問題。
回顧老闆的需求:「幫我看一下,每個組員在自己的組別領域裡面,還有哪些技術是不會的。」
思路是這樣的:
- 先列出每個組員「應該要會」的所有技術(組別領域的所有技術)
- 再列出每個組員「已經會」的技術
- 用差集把「已經會的」從「應該要會的」扣掉,剩下的就是「還不會的」
第一步:列出每個組員應該要會的技術
組員屬於某個組別,組別有領域,技術也有領域。
只要組別的領域和技術的領域一樣,就代表這個組員應該要會這個技術。
把組員 → 組別 → 技術串起來,就能得到每個組員應該要會的所有技術:
SELECT 組員.名稱 AS 組員名稱, 技術.名稱 AS 技術名稱
FROM 組員
LEFT JOIN 組別 ON 組員.組別ID = 組別.組別ID
LEFT JOIN 技術 ON 組別.領域 = 技術.領域注意這裡的合併條件:組別.領域 = 技術.領域。
不是用 ID 合併,而是用領域合併,這樣就能把同一個領域的所有技術都配對上。
結果(A 表 — 應該要會的):
Alice 和 Bob 屬於前端一組(Web 領域),所以配到 TypeScript 和 React。
Carol 和 Frank 屬於前端二組(也是 Web 領域),同樣配到 TypeScript 和 React。
Dave 屬於 AI 團隊(機器學習領域),配到 Python。
Eve 屬於 App 團隊(App 領域),配到 Swift。
第二步:列出每個組員已經會的技術
把組員 → 人員技術 → 技術串起來:
SELECT 組員.名稱 AS 組員名稱, 技術.名稱 AS 技術名稱
FROM 組員
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID結果(B 表 — 已經會的):
Frank 沒有出現在這張表裡,因為他在人員技術表中沒有任何紀錄。
第三步:用 EXCEPT 扣掉
A 表是「應該要會的」,B 表是「已經會的」。
A EXCEPT B = 應該要會的 – 已經會的 = 還不會的。
注意方向:大的放上面(A 表),要扣掉的放下面(B 表)。
SELECT 組員.名稱 AS 組員名稱, 技術.名稱 AS 技術名稱
FROM 組員
LEFT JOIN 組別 ON 組員.組別ID = 組別.組別ID
LEFT JOIN 技術 ON 組別.領域 = 技術.領域
EXCEPT
SELECT 組員.名稱 AS 組員名稱, 技術.名稱 AS 技術名稱
FROM 組員
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID結果:
Bob 和 Carol 都在 Web 領域,應該要會 TypeScript 和 React。
Bob 已經會 React,但還不會 TypeScript。
Carol 也是已經會 React,但還不會 TypeScript。
Frank 也在 Web 領域,但他什麼都還沒學,所以 TypeScript 和 React 都出現在結果裡。
其他人(Alice、Dave、Eve)在自己的領域裡都已經學完了,所以不會出現在結果中。
這就是前一篇用 <> 做不到的事情。
<> 是在排列組合的過程中逐筆排除,多對多的時候排不乾淨。
EXCEPT 是先把兩組結果各自算好,再整批扣掉,所以不會有「第一筆排掉、第二筆又帶回來」的問題。
欄位別名很重要
上面的 SQL 裡,兩組查詢都有 組員.名稱 和 技術.名稱。
但這兩個欄位原本都叫「名稱」,如果不取別名,EXCEPT 在比對的時候會搞混。
所以一定要用 AS 組員名稱 和 AS 技術名稱 把欄位名稱區分開來。
而且上下兩組查詢的別名必須一致:上面叫 組員名稱,下面也要叫 組員名稱,這樣 EXCEPT 才知道要拿哪一欄去比對。
這個規則對 UNION 和 INTERSECT 也一樣適用。
小結
這篇學到了三件事:
垂直整併不需要合併條件:
JOIN 是把欄位橫向接起來(水平),需要 ON 條件來配對。
UNION、INTERSECT、EXCEPT 是把資料列接在一起(垂直),不需要配對條件,但欄位的數量、名稱和型別必須一致。
三種整併方式:
UNION(聯集):兩邊都要,預設去除重複。
INTERSECT(交集):只要兩邊都有的。
EXCEPT(差集):從 A 扣掉 B,有方向性。
三種都可以加上 ALL 來保留重複的資料列。
EXCEPT 能解決 <> 排不掉的問題:
多對多關係下,<> 逐筆排除會排不乾淨。
EXCEPT 是先把兩組結果各自算好,再整批扣掉,不會有遺漏。