Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

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

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

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

跨表單查詢:用合併鏈串接多張表

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

前幾篇我們學了 JOIN 的基本用法,都是兩張表合併在一起。

但實務上,你經常需要把三張、四張、甚至更多張表合併在一起。

這篇來看怎麼把多張表串成一條合併鏈,並且用一個完整的實戰練習來演練。

練習:找出沒有人會 TypeScript 的 Web 組別

老闆說:「我們有好幾個 Web 領域的組別,但我擔心有些組裡面沒有人會 TypeScript。幫我把這些組找出來。」

換句話說,老闆要的是同時符合兩個條件的組別:

  1. 領域是 Web
  2. 組裡面沒有任何一個組員會 TypeScript

這次的情境是一個工程團隊的管理系統,總共有四張表:

組別表:

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

每個組別有自己的名稱和所屬領域(Web、機器學習、App 等)。

組員表:

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

一個組可以有很多組員,但一個組員只能隸屬於一個組別。

所以組員表上直接放了組別ID(Foreign Key),指向組別表。

技術表:

技術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

一個組員可以會很多技術,一個技術也可以被很多組員擁有。

這是多對多的關係,所以需要這張關聯表來記錄「誰會什麼技術」。

分析老闆的需求

回到老闆的問題:找出領域是 Web,而且沒有任何組員會 TypeScript 的組別。

要回答這個問題,你的腦袋會自然做這樣的推理:

  1. 先看哪些組別的領域是 Web → 需要查組別表
  2. 這些組別裡面有哪些組員 → 需要查組員表
  3. 這些組員分別會什麼技術 → 需要查人員技術表和技術表
  4. 有沒有人會 TypeScript?如果沒有,這個組別就是答案

每一步都需要不同的表,所以四張表必須全部串在一起。

但四張表要怎麼串?一步一步來。

解題思路:先做合併,把資料組齊

看到這種需要多張表的問題,不要急著想怎麼篩選。

先把資料組齊再說。

為什麼?因為如果你的資料不齊全,後面的篩選和計算都做不了。

我們最後要挑出的是組別,所以把組別放在最左邊。

這樣不管後面怎麼合併,所有組別都會被保留下來(還記得上一篇的 LEFT JOIN 嗎?左邊的資料全部保留)。

然後從組別出發,一路 LEFT JOIN,把其他表的資料一張一張接上來。

但要從哪張表開始接?我們一步一步來看。

用 LEFT JOIN 串接四張表:合併鏈逐步拆解

第一個 LEFT JOIN:組別 + 組員

FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID

組別跟誰有直接關係?只有組員表。

因為組員表上有組別ID,所以可以直接合併。

合併結果:

組別ID組別名稱領域組員ID組員名稱
1前端一組Web1Alice
1前端一組Web2Bob
2前端二組Web3Carol
3AI 團隊機器學習4Dave
4App 團隊App5Eve
組別名稱前端一組
領域Web
組員ID1
組員名稱Alice
組別名稱前端一組
領域Web
組員ID2
組員名稱Bob
組別名稱前端二組
領域Web
組員ID3
組員名稱Carol
組別名稱AI 團隊
領域機器學習
組員ID4
組員名稱Dave
組別名稱App 團隊
領域App
組員ID5
組員名稱Eve

現在我們知道每個組別裡面有哪些組員了。

第二個 LEFT JOIN:加上人員技術表

FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID

下一步,我們需要知道這些組員會哪些技術。

但組員和技術是多對多的關係,不能直接合併。

必須先經過中間的關聯表(人員技術表),才能走到技術表。

合併結果:

組別ID組別名稱領域組員ID組員名稱技術ID
1前端一組Web1Alice1
1前端一組Web1Alice2
1前端一組Web2Bob2
2前端二組Web3Carol2
3AI 團隊機器學習4Dave3
4App 團隊App5Eve4
組別名稱前端一組
領域Web
組員ID1
組員名稱Alice
技術ID1
組別名稱前端一組
領域Web
組員ID1
組員名稱Alice
技術ID2
組別名稱前端一組
領域Web
組員ID2
組員名稱Bob
技術ID2
組別名稱前端二組
領域Web
組員ID3
組員名稱Carol
技術ID2
組別名稱AI 團隊
領域機器學習
組員ID4
組員名稱Dave
技術ID3
組別名稱App 團隊
領域App
組員ID5
組員名稱Eve
技術ID4

現在我們知道每個組員對應到哪些技術ID了。

但技術ID 只是一個數字,還看不出是什麼技術。

第三個 LEFT JOIN:加上技術表

FROM 組別
LEFT JOIN 組員 ON 組別.組別ID = 組員.組別ID
LEFT JOIN 人員技術 ON 組員.組員ID = 人員技術.組員ID
LEFT JOIN 技術 ON 人員技術.技術ID = 技術.技術ID

最後一步,把技術的名稱也補上來:

組別ID組別名稱領域組員ID組員名稱技術ID技術名稱
1前端一組Web1Alice1TypeScript
1前端一組Web1Alice2React
1前端一組Web2Bob2React
2前端二組Web3Carol2React
3AI 團隊機器學習4Dave3Python
4App 團隊App5Eve4Swift
組別名稱前端一組
領域Web
組員ID1
組員名稱Alice
技術ID1
技術名稱TypeScript
組別名稱前端一組
領域Web
組員ID1
組員名稱Alice
技術ID2
技術名稱React
組別名稱前端一組
領域Web
組員ID2
組員名稱Bob
技術ID2
技術名稱React
組別名稱前端二組
領域Web
組員ID3
組員名稱Carol
技術ID2
技術名稱React
組別名稱AI 團隊
領域機器學習
組員ID4
組員名稱Dave
技術ID3
技術名稱Python
組別名稱App 團隊
領域App
組員ID5
組員名稱Eve
技術ID4
技術名稱Swift

四張表全部串在一起了。

把三個步驟合在一起,就是完整的合併鏈:

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'

篩選結果:

組別ID組別名稱領域組員ID組員名稱技術名稱
1前端一組Web1AliceTypeScript
組別名稱前端一組
領域Web
組員ID1
組員名稱Alice
技術名稱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

結果:

組別ID
1

這就是「有人會 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」。

組別ID
1

外層用 NOT IN 把這些ID 排除掉。

原本的組別表長這樣:

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

排掉組別ID 1 之後:

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

再加上 AND 領域 = 'Web',就只剩下「Web 領域中沒有人會 TypeScript 的組別」。

結果:

組別ID名稱領域
2前端二組Web
名稱前端二組
領域Web

前端二組的 Carol 只會 React,不會 TypeScript,所以被挑了出來。

延伸練習:用 LEFT JOIN 合併子查詢,算出 TypeScript 人數

老闆又問了:「我不只想知道哪些組沒人會 TypeScript,我還想看每個 Web 組別裡面,會 TypeScript 的人到底有幾個。」

換句話說,老闆要的是一張表:

  1. 列出所有 Web 領域的組別
  2. 每個組別旁邊顯示「會 TypeScript 的人數」
  3. 如果沒人會,顯示 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

結果:

組別ID人數
11
人數1

怎麼只有一筆?其他組別去哪了?

回想一下合併鏈跑出來的資料:

組別ID組別名稱領域組員ID組員名稱技術ID技術名稱
1前端一組Web1Alice1TypeScript
1前端一組Web1Alice2React
1前端一組Web2Bob2React
2前端二組Web3Carol2React
3AI 團隊機器學習4Dave3Python
4App 團隊App5Eve4Swift
組別名稱前端一組
領域Web
組員ID1
組員名稱Alice
技術ID1
技術名稱TypeScript
組別名稱前端一組
領域Web
組員ID1
組員名稱Alice
技術ID2
技術名稱React
組別名稱前端一組
領域Web
組員ID2
組員名稱Bob
技術ID2
技術名稱React
組別名稱前端二組
領域Web
組員ID3
組員名稱Carol
技術ID2
技術名稱React
組別名稱AI 團隊
領域機器學習
組員ID4
組員名稱Dave
技術ID3
技術名稱Python
組別名稱App 團隊
領域App
組員ID5
組員名稱Eve
技術ID4
技術名稱Swift

加上 WHERE 技術.名稱 = 'TypeScript' 之後,只有技術名稱是 TypeScript 的那一筆會留下來:

組別ID組別名稱領域組員ID組員名稱技術ID技術名稱
1前端一組Web1Alice1TypeScript
組別名稱前端一組
領域Web
組員ID1
組員名稱Alice
技術ID1
技術名稱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。

像這樣:

組別ID名稱領域人數
1前端一組Web1
2前端二組WebNULL
3AI 團隊機器學習NULL
4App 團隊AppNULL
名稱前端一組
領域Web
人數1
名稱前端二組
領域Web
人數NULL
名稱AI 團隊
領域機器學習
人數NULL
名稱App 團隊
領域App
人數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 組別:

組別ID名稱領域人數
1前端一組Web1
2前端二組WebNULL
名稱前端一組
領域Web
人數1
名稱前端二組
領域Web
人數NULL

前端二組的人數還是 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'

結果:

組別ID名稱TypeScript人數
1前端一組1
2前端二組0
名稱前端一組
TypeScript人數1
名稱前端二組
TypeScript人數0

前端二組在子查詢裡找不到對應,人數原本是 NULL,被 COALESCE 補成 0。

另外,你可能注意到前一步還有的「領域」欄位,在最終結果裡消失了。

這是因為 SELECT 只選了組別ID、名稱、TypeScript人數這三個欄位。WHERE 可以用領域來篩選,但不代表它一定要出現在結果裡。

小結

這篇學到了兩個重要的觀念:

合併鏈:

JOIN 不是只能合併兩張表。

你可以從一張主要的表出發,一路 LEFT JOIN 下去,把需要的輔助表一張一張接上來。

遇到多對多的關係,必須先經過關聯表,才能走到目標表。

解題的思路:

不知道怎麼開始的時候,先做合併。

先用 SELECT * 把所有資料組出來,確認輸入正確,再去想篩選和計算。

輸入最重要,如果資料不齊全,後面的計算都做不了。

目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

資料庫

目錄

  • 練習:找出沒有人會 TypeScript 的 Web 組別
  • 分析老闆的需求
  • 解題思路:先做合併,把資料組齊
  • 用 LEFT JOIN 串接四張表:合併鏈逐步拆解
  • 第一個 LEFT JOIN:組別 + 組員
  • 第二個 LEFT JOIN:加上人員技術表
  • 第三個 LEFT JOIN:加上技術表
  • 用 WHERE 篩選出會 TypeScript 的資料
  • 用 GROUP BY 找出有 TypeScript 的組別ID
  • 用 NOT IN 排除組別,找出最終答案
  • 延伸練習:用 LEFT JOIN 合併子查詢,算出 TypeScript 人數
  • 先算出每個組別有幾個人會 TypeScript
  • 用 LEFT JOIN 合併回組別表
  • 帶入子查詢
  • 幫子查詢取別名
  • 加上 WHERE 篩選 Web 領域
  • 用 COALESCE 把 NULL 補成 0
  • 小結