Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

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

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

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

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

跨表單查詢:結合 JOIN 與 INTERSECT 的實戰應用

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

前面學了 JOIN 做水平合併,也學了 UNION、INTERSECT、EXCEPT 做垂直整併。

但這些都是單獨的小練習,實際工作中的需求通常更複雜——你可能需要同時用到子查詢、分組計算、多次合併,才能把老闆要的資料拼出來。

這篇就用一個補習班系統的例子,來示範怎麼把這些技巧組合起來解決真實的問題。

先來看補習班系統的資料結構

還記得之前在做資料架構的時候,我們幫補習班設計過一套資料表嗎?

現在就用這套表來練習。

據點表:

據點ID名稱
1敦南校
2站前校
名稱敦南校
名稱站前校

人員表:

人員ID名稱類型
1王老師老師
2李老師老師
3小明學生
4小華學生
5小美學生
名稱王老師
類型老師
名稱李老師
類型老師
名稱小明
類型學生
名稱小華
類型學生
名稱小美
類型學生

課程表:

課程ID科目年級學生上限
1自然320
2數學325
3英文220
科目自然
年級3
學生上限20
科目數學
年級3
學生上限25
科目英文
年級2
學生上限20

教室表:

教室ID據點ID名稱人數上限
11A教室30
21B教室15
32C教室25
據點ID1
名稱A教室
人數上限30
據點ID1
名稱B教室
人數上限15
據點ID2
名稱C教室
人數上限25

授課班級表:

班級ID課程ID老師ID教室ID
1111
2123
3212
課程ID1
老師ID1
教室ID1
課程ID1
老師ID2
教室ID3
課程ID2
老師ID1
教室ID2

授課班級表記錄的是一個「開班」的概念。

例如班級ID 1 這筆資料,代表的是「王老師在 A 教室開了一班三年級自然課」。

同一門課程可能有多個授課班級。

像三年級自然課(課程ID 1)就有兩個授課班級:一個是王老師在 A 教室開的班,另一個是李老師在 C 教室開的班。

選課表:

選課ID學生ID班級ID
131
241
352
學生ID3
班級ID1
學生ID4
班級ID1
學生ID5
班級ID2

選課表記錄的是「哪個學生選了哪個授課班級」。

為什麼是選授課班級,不是選課程?

因為同一門課程可能有好幾個班。

學生選課的時候,不是只說「我要上三年級自然課」,而是要選「王老師在 A 教室開的那一班」還是「李老師在 C 教室開的那一班」。

所以選課表連結的是學生和授課班級,不是學生和課程。

這幾張表彼此勾連,組成了補習班的核心系統。

用子查詢找出還沒滿班的授課班級

老闆說:「三年級自然課現在很夯,很多班都滿了。幫我挑出三年級自然課還沒滿班的授課班級,還有它是在哪個分校上課。」

先拆解問題

聽到這個需求,腦中要先想清楚幾件事:

  1. 我要挑的是「授課班級」,所以授課班級是主角
  2. 我要知道這個授課班級的課程是不是三年級自然科
  3. 我要知道這個課程的學生上限是多少
  4. 我要知道這個授課班級目前選課的人數有多少
  5. 比對選課人數和學生上限,就知道有沒有滿班

這可以分成三個步驟:

  1. 先挑出三年級自然科的授課班級,順便拿到學生上限
  2. 算出每個授課班級的選課人數
  3. 把兩個結果合併,篩選出還沒滿班的

第一步:挑出三年級自然科的授課班級

授課班級是主角,所以授課班級一定放在左邊。

授課班級表裡只有課程ID,沒有課程的詳細資料,所以要 JOIN 課程表。

SELECT 授課班級.班級ID, 課程.學生上限
FROM 授課班級
LEFT JOIN 課程 ON 授課班級.課程ID = 課程.課程ID
WHERE 課程.年級 = 3 AND 課程.科目 = '自然'

這樣就挑出三年級自然科的授課班級,還有它的學生上限。

結果:

班級ID學生上限
120
220
學生上限20
學生上限20

班級ID 1 和 2 都是三年級自然課(課程ID 1),所以被挑出來。

班級ID 3 是三年級數學課,不符合條件,沒有出現在結果裡。

第二步:算出每個授課班級的選課人數

要算人數,就要把授課班級和選課表合併,然後分組計算。

SELECT 授課班級.班級ID, COUNT(*) AS 學生人數
FROM 授課班級
LEFT JOIN 選課 ON 授課班級.班級ID = 選課.班級ID
GROUP BY 授課班級.班級ID

我們一步一步來看這個查詢做了什麼。

首先是 LEFT JOIN:

FROM 授課班級
LEFT JOIN 選課 ON 授課班級.班級ID = 選課.班級ID

結果:

班級ID課程ID老師ID教室ID選課ID學生ID
111113
111124
212335
3212NULLNULL
課程ID1
老師ID1
教室ID1
選課ID1
學生ID3
課程ID1
老師ID1
教室ID1
選課ID2
學生ID4
課程ID1
老師ID2
教室ID3
選課ID3
學生ID5
課程ID2
老師ID1
教室ID2
選課IDNULL
學生IDNULL

班級ID 1 有兩筆選課紀錄,所以產生兩列。

班級ID 3 沒有人選,但因為是 LEFT JOIN,還是會出現在結果裡,選課的欄位都是 NULL。

接著用 GROUP BY 分組:

FROM 授課班級
LEFT JOIN 選課 ON 授課班級.班級ID = 選課.班級ID
GROUP BY 授課班級.班級ID
班級ID(這組有幾列)
12 列
21 列
31 列
(這組有幾列)2 列
(這組有幾列)1 列
(這組有幾列)1 列

最後用 COUNT 算每組有幾列,就是學生人數:

班級ID學生人數
12
21
31
學生人數2
學生人數1
學生人數1

等等,班級ID 3 明明沒有人選,為什麼學生人數是 1?

因為 COUNT() 是算「這組有幾列」,班級ID 3 雖然選課欄位是 NULL,但還是有一列資料,所以 COUNT() 算出來是 1。

如果要正確計算,應該用 COUNT(選課.選課ID),這樣 NULL 就不會被算進去:

SELECT 授課班級.班級ID, COUNT(選課.選課ID) AS 學生人數
FROM 授課班級
LEFT JOIN 選課 ON 授課班級.班級ID = 選課.班級ID
GROUP BY 授課班級.班級ID

結果:

班級ID學生人數
12
21
30
學生人數2
學生人數1
學生人數0

這樣班級ID 3 的學生人數就正確顯示為 0 了。

第三步:合併兩個結果,篩選還沒滿班的

現在有兩個結果:

  • 結果一:三年級自然科的授課班級,還有學生上限
  • 結果二:每個授課班級的學生人數

把這兩個結果合併,比對學生人數和學生上限,就知道哪些還沒滿班。

先看整體結構:

SELECT 三年級自然科班級.班級ID
FROM (
    -- 第一步:挑出三年級自然科的授課班級
) AS 三年級自然科班級
LEFT JOIN (
    -- 第二步:算出每個授課班級的選課人數
) AS 班級學生人數
ON 三年級自然科班級.班級ID = 班級學生人數.班級ID
WHERE 學生人數 < 學生上限

圓括號裡面的就是子查詢,分別對應第一步和第二步。

把子查詢的內容填進去:

SELECT 三年級自然科班級.班級ID
FROM (
    SELECT 授課班級.班級ID, 課程.學生上限
    FROM 授課班級
    LEFT JOIN 課程 ON 授課班級.課程ID = 課程.課程ID
    WHERE 課程.年級 = 3 AND 課程.科目 = '自然'
) AS 三年級自然科班級
LEFT JOIN (
    SELECT 授課班級.班級ID, COUNT(選課.選課ID) AS 學生人數
    FROM 授課班級
    LEFT JOIN 選課 ON 授課班級.班級ID = 選課.班級ID
    GROUP BY 授課班級.班級ID
) AS 班級學生人數
ON 三年級自然科班級.班級ID = 班級學生人數.班級ID
WHERE 學生人數 < 學生上限

這裡用了子查詢:圓括號裡面的 SELECT 就是一個子查詢,它會先執行完,產生一個暫時的表,再拿來做合併。

為什麼從「三年級自然科班級」開始 JOIN?

注意看,我們是用「三年級自然科班級」去 LEFT JOIN「班級學生人數」。

「三年級自然科班級」只有班級ID 1 和 2(三年級自然科的授課班級)。

「班級學生人數」有班級ID 1、2、3(所有授課班級的學生人數)。

從「三年級自然科班級」開始 LEFT JOIN,出來的結果就只會有班級ID 1 和 2。

班級ID 3 雖然在「班級學生人數」裡面,但因為「三年級自然科班級」裡沒有它,所以不會出現在結果裡。

這樣就不用在最外層再加一個 WHERE 課程.年級 = 3 AND 課程.科目 = '自然' 來篩選了。

用 USING 簡化合併條件

上面的合併條件是這樣寫的:

ON 三年級自然科班級.班級ID = 班級學生人數.班級ID

兩邊的欄位名稱都是「班級ID」,可以用 USING 來簡化:

LEFT JOIN 班級學生人數 USING (班級ID)

USING 的意思是:左右兩張表都有這個欄位,用它來做合併。

這樣寫起來更簡潔,也更容易讀懂。

完整的 SQL

SELECT 三年級自然科班級.班級ID
FROM (
    SELECT 授課班級.班級ID, 課程.學生上限
    FROM 授課班級
    LEFT JOIN 課程 ON 授課班級.課程ID = 課程.課程ID
    WHERE 課程.年級 = 3 AND 課程.科目 = '自然'
) AS 三年級自然科班級
LEFT JOIN (
    SELECT 授課班級.班級ID, COUNT(選課.選課ID) AS 學生人數
    FROM 授課班級
    LEFT JOIN 選課 ON 授課班級.班級ID = 選課.班級ID
    GROUP BY 授課班級.班級ID
) AS 班級學生人數
USING (班級ID)
WHERE 學生人數 < 學生上限

用 INTERSECT 找出需要換教室的授課班級

老闆又說:「有些授課班級選課人數還不到 10 個人,卻用了一間大教室(人數上限超過 20 人)。幫我把這些授課班級挑出來,我要讓它們換教室。」

換個思路:用交集來解

這個需求其實是兩個條件的交集:

  1. 選課人數不到 10 人的授課班級
  2. 教室人數上限超過 20 人的授課班級

同時符合這兩個條件的授課班級,就是我們要找的。

如果用 JOIN 的方式,你要先算人數,再合併教室表,再篩選,會有點複雜。

用 INTERSECT 就簡單多了:分別查出兩組結果,再取交集。

第一步:找出選課人數不到 10 人的授課班級

一步一步來看怎麼寫這個查詢。

首先是 LEFT JOIN:

FROM 授課班級
LEFT JOIN 選課 ON 授課班級.班級ID = 選課.班級ID

結果:

班級ID課程ID老師ID教室ID選課ID學生ID
111113
111124
212335
3212NULLNULL
課程ID1
老師ID1
教室ID1
選課ID1
學生ID3
課程ID1
老師ID1
教室ID1
選課ID2
學生ID4
課程ID1
老師ID2
教室ID3
選課ID3
學生ID5
課程ID2
老師ID1
教室ID2
選課IDNULL
學生IDNULL

接著用 GROUP BY 分組:

FROM 授課班級
LEFT JOIN 選課 ON 授課班級.班級ID = 選課.班級ID
GROUP BY 授課班級.班級ID
班級ID(這組有幾列)
12 列
21 列
31 列
(這組有幾列)2 列
(這組有幾列)1 列
(這組有幾列)1 列

最後用 HAVING 篩選出 COUNT(選課.選課ID) < 10 的:

SELECT 授課班級.班級ID
FROM 授課班級
LEFT JOIN 選課 ON 授課班級.班級ID = 選課.班級ID
GROUP BY 授課班級.班級ID
HAVING COUNT(選課.選課ID) < 10

結果:

班級ID
1
2
3

三個班級的選課人數都不到 10 人,所以全部都符合條件。

這裡用 HAVING 而不是 WHERE,因為我們是根據分組計算的結果來篩選。

WHERE 是在分組之前篩選,HAVING 是在分組之後篩選。

第二步:找出教室人數上限超過 20 人的授課班級

一樣一步一步來。

首先是 LEFT JOIN:

FROM 授課班級
LEFT JOIN 教室 ON 授課班級.教室ID = 教室.教室ID

結果:

班級ID課程ID老師ID教室ID據點ID教室名稱人數上限
11111A教室30
21232C教室25
32121B教室15
課程ID1
老師ID1
教室ID1
據點ID1
教室名稱A教室
人數上限30
課程ID1
老師ID2
教室ID3
據點ID2
教室名稱C教室
人數上限25
課程ID2
老師ID1
教室ID2
據點ID1
教室名稱B教室
人數上限15

接著用 WHERE 篩選人數上限超過 20 的:

SELECT 授課班級.班級ID
FROM 授課班級
LEFT JOIN 教室 ON 授課班級.教室ID = 教室.教室ID
WHERE 教室.人數上限 > 20

結果:

班級ID
1
2

班級ID 1 的教室是 A 教室(人數上限 30),班級ID 2 的教室是 C 教室(人數上限 25),都超過 20。

班級ID 3 的教室是 B 教室(人數上限 15),不超過 20,所以被篩掉了。

第三步:取交集

SELECT 授課班級.班級ID
FROM 授課班級
LEFT JOIN 選課 ON 授課班級.班級ID = 選課.班級ID
GROUP BY 授課班級.班級ID
HAVING COUNT(選課.選課ID) < 10

INTERSECT

SELECT 授課班級.班級ID
FROM 授課班級
LEFT JOIN 教室 ON 授課班級.教室ID = 教室.教室ID
WHERE 教室.人數上限 > 20

INTERSECT 會找出同時出現在上下兩個結果中的班級ID。

這些就是「選課人數不到 10 人,但用了大教室」的授課班級。

結果:

班級ID
1
2

第一步的結果是班級ID 1、2、3,第二步的結果是班級ID 1、2。

交集就是 1 和 2。

巢狀結構很複雜怎麼辦?

你可能已經發現,當子查詢越來越多,整個 SQL 會變得很難讀。

像任務一的完整 SQL,光是看那一層一層的括號,就讓人頭暈。

如果你覺得這個巢狀結構很不舒服,想要有一個乾淨易讀的版本,下一篇會教你怎麼用 CTE(Common Table Expression)來整理這些子查詢。

小結

這篇學到了三件事:

從主角開始思考:

拿到需求,先想清楚「我要挑的是什麼」。

那個東西就是主角,一定放在 FROM 的最左邊。

拆解問題,分步驟處理:

複雜的需求不要想一次解決。

先拆成幾個小步驟,各自寫成子查詢,最後再合併起來。

善用 INTERSECT 簡化邏輯:

當需求是「同時符合多個條件」的時候,用 INTERSECT 取交集,比用一堆 JOIN 和 WHERE 更直覺。

上一篇跨表單查詢:用 UNION、INTERSECT、EXCEPT 做資料列的垂直整併
下一篇SQL 可讀性:為什麼巢狀查詢讓人想死
目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

資料庫

目錄

  • 先來看補習班系統的資料結構
  • 用子查詢找出還沒滿班的授課班級
  • 先拆解問題
  • 第一步:挑出三年級自然科的授課班級
  • 第二步:算出每個授課班級的選課人數
  • 第三步:合併兩個結果,篩選還沒滿班的
  • 為什麼從「三年級自然科班級」開始 JOIN?
  • 用 USING 簡化合併條件
  • 完整的 SQL
  • 用 INTERSECT 找出需要換教室的授課班級
  • 換個思路:用交集來解
  • 第一步:找出選課人數不到 10 人的授課班級
  • 第二步:找出教室人數上限超過 20 人的授課班級
  • 第三步:取交集
  • 巢狀結構很複雜怎麼辦?
  • 小結