Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

本文為「SQL 資料庫新手村」系列第 54 篇

跨表單查詢:用 UNION、INTERSECT、EXCEPT 做資料列的垂直整併

最後更新:2026年2月5日資料庫

前面學的 JOIN 是「水平合併」——把不同表的欄位橫向接起來,讓一列資料變得更寬。

但有些時候,我們需要的不是把欄位橫向接起來,而是把一張表的資料列,接在另一張表的下面。

例如 A 表查出 3 筆結果:

名稱
Alice
Bob
Carol

B 表查出 2 筆結果:

名稱
Dave
Eve

我想把這 5 筆放在同一張表裡:

名稱
Alice
Bob
Carol
Dave
Eve

就是把 B 表的資料列,直接接在 A 表的下面。

這種操作叫做垂直整併。

垂直整併比 JOIN 簡單,因為不需要設合併條件——你就是把兩組資料接在一起而已。

SQL 提供了三種垂直整併的方式:聯集(UNION)、交集(INTERSECT)、差集(EXCEPT)。

先來看為什麼需要垂直整併

老闆說:「我們現在要全面發展 Web 方向,機器學習先暫緩。」

「幫我列出所有跟 Web 有關的組員。有兩種情況:一種是組別領域是 Web 的,另一種是技術領域有 Web 的。兩種都幫我挑出來,放在一張清單裡。」

沿用前幾篇的四張表:

組別表:

組別ID名稱領域
1前端一組Web
2前端二組Web
3AI 團隊機器學習
4App 團隊App
名稱前端一組
領域Web
名稱前端二組
領域Web
名稱AI 團隊
領域機器學習
名稱App 團隊
領域App

組員表:

組員ID名稱組別ID
1Alice1
2Bob1
3Carol2
4Dave3
5Eve4
6Frank2
名稱Alice
組別ID1
名稱Bob
組別ID1
名稱Carol
組別ID2
名稱Dave
組別ID3
名稱Eve
組別ID4
名稱Frank
組別ID2

技術表:

技術ID名稱領域
1TypeScriptWeb
2ReactWeb
3Python機器學習
4SwiftApp
名稱TypeScript
領域Web
名稱React
領域Web
名稱Python
領域機器學習
名稱Swift
領域App

人員技術表:

組員ID技術ID
11
12
22
32
43
54
技術ID1
技術ID2
技術ID2
技術ID2
技術ID3
技術ID4

策略一:全部 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 筆):

組員ID組員名稱組別名稱組別領域技術名稱技術領域
1Alice前端一組WebTypeScriptWeb
1Alice前端一組WebReactWeb
2Bob前端一組WebReactWeb
3Carol前端二組WebReactWeb
4DaveAI 團隊機器學習Python機器學習
5EveApp 團隊AppSwiftApp
6Frank前端二組WebNULLNULL
組員名稱Alice
組別名稱前端一組
組別領域Web
技術名稱TypeScript
技術領域Web
組員名稱Alice
組別名稱前端一組
組別領域Web
技術名稱React
技術領域Web
組員名稱Bob
組別名稱前端一組
組別領域Web
技術名稱React
技術領域Web
組員名稱Carol
組別名稱前端二組
組別領域Web
技術名稱React
技術領域Web
組員名稱Dave
組別名稱AI 團隊
組別領域機器學習
技術名稱Python
技術領域機器學習
組員名稱Eve
組別名稱App 團隊
組別領域App
技術名稱Swift
技術領域App
組員名稱Frank
組別名稱前端二組
組別領域Web
技術名稱NULL
技術領域NULL

Frank 因為沒有任何技術紀錄,LEFT JOIN 之後技術名稱和技術領域都是 NULL。

再用 WHERE 組別.領域 = 'Web' OR 技術.領域 = 'Web' 篩選,Dave 和 Eve 被排掉,Frank 因為組別領域是 Web 所以留下來:

組員ID組員名稱
1Alice
1Alice
2Bob
3Carol
6Frank
組員名稱Alice
組員名稱Alice
組員名稱Bob
組員名稱Carol
組員名稱Frank

結果是對的,但有一個問題:效能不好。

回頭看一下老闆的需求,其實是兩件獨立的事:

  1. 組別領域是 Web 的組員
  2. 技術領域有 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 的組員):

組員ID名稱
1Alice
2Bob
3Carol
6Frank
名稱Alice
名稱Bob
名稱Carol
名稱Frank

下面的查詢結果(技術領域有 Web 的組員):

組員ID名稱
1Alice
1Alice
2Bob
3Carol
名稱Alice
名稱Alice
名稱Bob
名稱Carol

Frank 不在下面的結果裡,因為他沒有任何技術紀錄,自然也沒有 Web 技術。

UNION 接在一起並去除重複後:

組員ID名稱
1Alice
2Bob
3Carol
6Frank
名稱Alice
名稱Bob
名稱Carol
名稱Frank

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 的組員):

組員ID名稱
1Alice
2Bob
3Carol
6Frank
名稱Alice
名稱Bob
名稱Carol
名稱Frank

下面的查詢結果(技術領域有 Web 的組員):

組員ID名稱
1Alice
2Bob
3Carol
名稱Alice
名稱Bob
名稱Carol

INTERSECT 只留下兩邊都有的:

組員ID名稱
1Alice
2Bob
3Carol
名稱Alice
名稱Bob
名稱Carol

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 的組員):

組員ID名稱
1Alice
2Bob
3Carol
6Frank
名稱Alice
名稱Bob
名稱Carol
名稱Frank

下面的查詢結果(技術領域有 Web 的組員):

組員ID名稱
1Alice
2Bob
3Carol
名稱Alice
名稱Bob
名稱Carol

EXCEPT 從上面的結果扣掉下面也有的,只留下上面獨有的:

組員ID名稱
6Frank
名稱Frank

Alice、Bob、Carol 同時出現在上下兩邊,被 EXCEPT 扣掉了,只剩下 Frank。

但老闆要的是所有跟 Web 有關的人,不是「只在組別領域是 Web、但沒有 Web 技術」的人。

EXCEPT 的結果只有 Frank 一個人,少了 Alice、Bob、Carol,完全不符合需求。

所以這個情境也不適合用 EXCEPT。

EXCEPT 適合的場景是:你想從 A 裡面排掉 B 已經有的,只留下 A 獨有的部分。

用 EXCEPT 解決 <> 排不掉的問題

前一篇文章提到,用 <> 找「組員還不會的技術」會失敗,因為多對多的關係下 <> 排不乾淨。

現在用 EXCEPT 就能解決這個問題。

回顧老闆的需求:「幫我看一下,每個組員在自己的組別領域裡面,還有哪些技術是不會的。」

思路是這樣的:

  1. 先列出每個組員「應該要會」的所有技術(組別領域的所有技術)
  2. 再列出每個組員「已經會」的技術
  3. 用差集把「已經會的」從「應該要會的」扣掉,剩下的就是「還不會的」

第一步:列出每個組員應該要會的技術

組員屬於某個組別,組別有領域,技術也有領域。

只要組別的領域和技術的領域一樣,就代表這個組員應該要會這個技術。

把組員 → 組別 → 技術串起來,就能得到每個組員應該要會的所有技術:

SELECT 組員.名稱 AS 組員名稱, 技術.名稱 AS 技術名稱
FROM 組員
LEFT JOIN 組別 ON 組員.組別ID = 組別.組別ID
LEFT JOIN 技術 ON 組別.領域 = 技術.領域

注意這裡的合併條件:組別.領域 = 技術.領域。

不是用 ID 合併,而是用領域合併,這樣就能把同一個領域的所有技術都配對上。

結果(A 表 — 應該要會的):

組員名稱技術名稱
AliceTypeScript
AliceReact
BobTypeScript
BobReact
CarolTypeScript
CarolReact
DavePython
EveSwift
FrankTypeScript
FrankReact
技術名稱TypeScript
技術名稱React
技術名稱TypeScript
技術名稱React
技術名稱TypeScript
技術名稱React
技術名稱Python
技術名稱Swift
技術名稱TypeScript
技術名稱React

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 表 — 已經會的):

組員名稱技術名稱
AliceTypeScript
AliceReact
BobReact
CarolReact
DavePython
EveSwift
技術名稱TypeScript
技術名稱React
技術名稱React
技術名稱React
技術名稱Python
技術名稱Swift

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

結果:

組員名稱技術名稱
BobTypeScript
CarolTypeScript
FrankTypeScript
FrankReact
技術名稱TypeScript
技術名稱TypeScript
技術名稱TypeScript
技術名稱React

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 是先把兩組結果各自算好,再整批扣掉,不會有遺漏。

上一篇跨表單查詢:JOIN 的執行順序是什麼?
下一篇跨表單查詢:結合 JOIN 與 INTERSECT 的實戰應用
目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

資料庫

目錄

  • 先來看為什麼需要垂直整併
  • 策略一:全部 JOIN 在一起再篩選
  • 策略二:分開查,再接在一起
  • 聯集(UNION):兩邊都要
  • 欄位必須一致
  • 交集(INTERSECT):只要重疊的
  • 同樣的需求,換成 INTERSECT 會怎樣?
  • 差集(EXCEPT):從 A 扣掉 B
  • 同樣的需求,換成 EXCEPT 會怎樣?
  • 用 EXCEPT 解決 <> 排不掉的問題
  • 第一步:列出每個組員應該要會的技術
  • 第二步:列出每個組員已經會的技術
  • 第三步:用 EXCEPT 扣掉
  • 欄位別名很重要
  • 小結