Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

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

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

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

跨表單查詢:用 JOIN 合併兩張表(正式寫法)

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

上一篇我們學了用逗號合併兩張表,但發現一個問題:合併條件和篩選條件混在一起,很難維護。

這篇來學正式的合併指令:JOIN。

它做的事情跟上一篇一模一樣,只是寫法比較漂亮。

為什麼需要專門的合併指令?

上一篇我們學了用逗號合併兩張表,結果是對的,但寫法有點醜。

你可能會想:「結果對就好啦,幹嘛還要學新的指令?」

這就要講到工程上的一個重要觀念:語意化。

什麼是語意化?

簡單說,就是「寫人話」。

讓你的程式碼一看就知道在做什麼,不用猜。

這樣做的好處是:

  • 你自己看得懂
  • 你的同事看得懂
  • 三個月後的你自己還是看得懂

上一篇的問題

上一篇的寫法:

FROM 會員, 訂單
WHERE 會員.會員ID = 訂單.會員ID

乍看之下,不容易一眼看出「這是在做合併」。

WHERE 裡面可能有很多條件,你要仔細看才會發現「喔,原來這一行是合併條件」。

JOIN 的基本語法

用 JOIN 改寫後:

FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID

JOIN 這個字在英文是「加入」或「聯合」的意思。

想像兩個人「join hands」(牽手),就是把兩隻手接在一起。

在 SQL 裡面,JOIN 就是把兩張表「接在一起」,也就是合併。

一看到 JOIN 這個字,就知道:這裡在做表的合併。

意圖非常清楚,不用猜。

語法結構

表單1 JOIN 表單2 ON 合併條件

JOIN 的意思是「聯合」或「加入」,把兩張表接在一起。

ON 後面放的是合併條件,告訴資料庫「要怎麼接」。

JOIN 和 ON 是一組的,不會分開。

跟上一篇的差別

上一篇(逗號)這一篇(JOIN)
FROM 會員, 訂單FROM 會員 JOIN 訂單
WHERE 會員.會員ID = 訂單.會員IDON 會員.會員ID = 訂單.會員ID
這一篇(JOIN)FROM 會員 JOIN 訂單
這一篇(JOIN)ON 會員.會員ID = 訂單.會員ID

合併條件從 WHERE 搬到 ON,僅此而已。

JOIN 的好處:合併條件獨立出來

現在合併條件放在 ON 後面,不會跟 WHERE 的篩選條件混在一起。

來看一個複雜一點的例子:

上一篇的寫法:

SELECT *
FROM 會員, 訂單
WHERE 會員.會員ID = 訂單.會員ID
  AND 訂單.金額 >= 25000
  AND 會員.等級 = 'VIP'

合併條件和篩選條件全部混在 WHERE 裡面。

這一篇的寫法:

SELECT *
FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
WHERE 訂單.金額 >= 25000
  AND 會員.等級 = 'VIP'

現在很清楚:

  • ON 後面:合併條件(決定兩張表怎麼接)
  • WHERE 後面:篩選條件(決定要保留哪些資料)

未來要修改的時候,一眼就能分辨。

JOIN 多張表:一次合併三張以上的表

實務上,你可能需要合併不只兩張表。

例如:會員表、訂單表、商品表、分類表… 一路合併下去。

用逗號的寫法會變成這樣:

FROM 會員, 訂單, 商品, 分類
WHERE 會員.會員ID = 訂單.會員ID
  AND 訂單.商品ID = 商品.商品ID
  AND 商品.分類ID = 分類.分類ID

四張表,三個合併條件,全部塞在 WHERE 裡面。

如果還有篩選條件,會更難讀。

用 JOIN 改寫:

FROM 會員
JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
JOIN 商品 ON 訂單.商品ID = 商品.商品ID
JOIN 分類 ON 商品.分類ID = 分類.分類ID

每個 JOIN 後面都跟著自己的 ON,一對一對的,非常清楚。

你可以一眼看出:

  • 會員 和 訂單 怎麼接
  • 訂單 和 商品 怎麼接
  • 商品 和 分類 怎麼接

這比用逗號隔開再寫一堆 WHERE 條件清楚多了。

實作:把逗號寫法改成 JOIN

現在來把上一篇的查詢改成 JOIN 寫法。

上一篇我們寫了一個查詢,用逗號合併會員和訂單表,然後計算平均客單價:

SELECT 姓名, 信箱
FROM (
    SELECT *,
        AVG(金額) OVER (PARTITION BY 會員.會員ID) AS 客單價
    FROM 會員, 訂單
    WHERE 會員.會員ID = 訂單.會員ID
      AND 訂單.金額 > 25000
) AS 會員_訂單
WHERE 客單價 > 50000

問題出在子查詢的 WHERE 裡面,合併條件和篩選條件混在一起。

接下來我們一步一步把它改成 JOIN 寫法。

步驟一:把 FROM 逗號改成 JOIN ON

把 FROM 會員, 訂單 改成 FROM 會員 JOIN 訂單。

把合併條件從 WHERE 移到 ON。

修改前:

FROM 會員, 訂單
WHERE 會員.會員ID = 訂單.會員ID
  AND 訂單.金額 > 25000

修改後:

FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
WHERE 訂單.金額 > 25000

現在合併條件和篩選條件分開了:

  • ON:合併條件(會員.會員ID = 訂單.會員ID)
  • WHERE:篩選條件(訂單.金額 > 25000)

步驟二:幫合併結果取別名

在 SQL 裡,很多操作執行完都會產生一張表。

例如 SELECT ... FROM ... 執行完會產生一張表(查詢結果)。

會員 JOIN 訂單 ON ... 也一樣,執行完會產生一張「臨時表單」,包含了兩張表合併後的所有資料。

如果你要在這張臨時表單上做其他操作(例如篩選、計算窗函數),需要幫它取一個別名。

為什麼?因為後面的 WHERE、SELECT 在引用欄位的時候,需要知道「我要從哪張表拿資料」。

取了別名之後,就可以用 別名.欄位名稱 的方式來引用。

修改前:

FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
WHERE 訂單.金額 > 25000

修改後:

FROM (
    會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
) AS 會員_訂單_原始
WHERE 會員_訂單_原始.金額 > 25000

步驟三:解決 ambiguous 欄位名稱重複問題

經過前兩個步驟,目前的 SQL 長這樣:

SELECT 姓名, 信箱
FROM (
    SELECT *,
        AVG(金額) OVER (PARTITION BY 會員.會員ID) AS 客單價
    FROM (
        會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
    ) AS 會員_訂單_原始
    WHERE 會員_訂單_原始.金額 > 25000
) AS 會員_訂單
WHERE 客單價 > 50000

現在有一個問題:窗函數的 PARTITION BY 會員.會員ID 也要改。

因為我們已經把 JOIN 的結果取名為 會員_訂單_原始,所以後面引用欄位的時候,表單名稱也要跟著改。

例如 會員.會員ID 要改成 會員_訂單_原始.會員ID。

但這裡會遇到一個問題。

當你執行這段程式碼的時候,資料庫會報錯:

ERROR: column reference "會員ID" is ambiguous

ambiguous 的意思是「模糊不清」。

為什麼會報這個錯?

因為會員表有「會員ID」,訂單表也有「會員ID」,合併之後,這張臨時表單裡面有兩個「會員ID」。

回憶一下原本的表格:

會員表:

會員ID姓名信箱
1Johnjohn@mail.com
2Marymary@mail.com
3Tomtom@mail.com
姓名John
信箱john@mail.com
姓名Mary
信箱mary@mail.com
姓名Tom
信箱tom@mail.com

訂單表:

訂單ID會員ID金額
1160,000
2120,000
3255,000
會員ID1
金額60,000
會員ID1
金額20,000
會員ID2
金額55,000

兩張表都有「會員ID」這個欄位。合併之後,資料庫不知道你要的是哪一個。

當你寫 會員_訂單_原始.會員ID 的時候,資料庫不知道你指的是哪一個,所以報錯說「這個欄位名稱 ambiguous(模糊不清)」。

解決方法:用 SELECT 只挑出需要的欄位

現在 JOIN 的結果會直接把兩張表的所有欄位都合併在一起,所以會有兩個「會員ID」。

解決方法是:在 JOIN 之後,用 SELECT 只挑出我們需要的欄位,這樣就可以避開重複的欄位。

有兩種做法:

做法一:只挑出需要的欄位,避開重複

我們只需要:會員ID、信箱、金額。

把這三個欄位挑出來,就不會有重複的問題。

修改前:

FROM (
    會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
) AS 會員_訂單_原始

修改後:

FROM (
    SELECT 會員.會員ID, 信箱, 金額
    FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
) AS 會員_訂單_原始

在 SELECT 裡面,我們寫的是 會員.會員ID,明確指定要會員表的會員ID,這樣就不會 ambiguous 了。

你可能會問:為什麼挑會員表的,不挑訂單表的?

其實兩邊都可以,因為我們在 JOIN 的時候已經用 會員.會員ID = 訂單.會員ID 作為合併條件,所以這兩個欄位的值一定是一樣的。

挑哪一邊都會得到相同的結果,這裡只是要避開 ambiguous 的問題。

做法二:把重複的欄位改名

如果你真的需要兩邊的會員ID,就用 AS 把它們改成不同的名字:

SELECT 
    會員.會員ID AS 會員_會員ID, 
    訂單.會員ID AS 訂單_會員ID, 
    信箱, 
    金額
FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID

這篇我們用做法一,因為比較簡單。

步驟四:省略不必要的表單名稱

經過前三個步驟,目前的 SQL 長這樣:

SELECT 姓名, 信箱
FROM (
    SELECT *,
        AVG(金額) OVER (PARTITION BY 會員_訂單_原始.會員ID) AS 客單價
    FROM (
        SELECT 會員.會員ID, 信箱, 金額
        FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
    ) AS 會員_訂單_原始
    WHERE 會員_訂單_原始.金額 > 25000
) AS 會員_訂單
WHERE 客單價 > 50000

因為我們在內層的 SELECT 已經把 會員.會員ID 挑出來了,這張臨時表單裡面現在只有一個 會員ID 欄位,不會有重複的問題。

既然欄位名稱不重複,外層在引用的時候就不需要再寫表單名稱了。

像是 PARTITION BY 會員_訂單_原始.會員ID 可以簡化成 PARTITION BY 會員ID。

WHERE 會員_訂單_原始.金額 > 25000 也可以簡化成 WHERE 金額 > 25000。

因為資料庫知道這張表裡面只有一個 會員ID、一個 金額,不會搞混。

修改前:

AVG(金額) OVER (PARTITION BY 會員_訂單_原始.會員ID) AS 客單價
...
WHERE 會員_訂單_原始.金額 > 25000

修改後:

AVG(金額) OVER (PARTITION BY 會員ID) AS 客單價
...
WHERE 金額 > 25000

因為欄位名稱沒有重複,所以連表單名稱都不用寫。

步驟五:簡化巢狀子查詢

經過前四個步驟,目前的 SQL 長這樣:

SELECT 姓名, 信箱
FROM (
    SELECT *,
        AVG(金額) OVER (PARTITION BY 會員ID) AS 客單價
    FROM (
        SELECT 會員.會員ID, 信箱, 金額
        FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
    ) AS 會員_訂單_原始
    WHERE 金額 > 25000
) AS 會員_訂單
WHERE 客單價 > 50000

你會發現這個查詢有三層:

  • 最內層:SELECT 會員.會員ID, 信箱, 金額 FROM 會員 JOIN 訂單 ...(合併表單、挑出欄位)
  • 中間層:SELECT *, AVG(金額) OVER ... FROM (最內層) WHERE 金額 > 25000(計算窗函數、篩選金額)
  • 最外層:SELECT 姓名, 信箱 FROM (中間層) WHERE 客單價 > 50000(篩選客單價、挑出結果)

看起來有點複雜。

但其實最內層和中間層可以合併。

為什麼?

因為最內層和中間層做的事情,其實可以寫在同一個查詢裡。

來看看這兩層分別在做什麼:

最內層:

SELECT 會員.會員ID, 信箱, 金額
FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID

做的事情是:合併表單、挑出欄位。

中間層:

SELECT *,
    AVG(金額) OVER (PARTITION BY 會員ID) AS 客單價
FROM (最內層) AS 會員_訂單_原始
WHERE 金額 > 25000

做的事情是:計算窗函數、篩選金額。

這兩層可以合併成一層。

為什麼?因為中間層的 SELECT * 只是把最內層的欄位全部拿出來,再加上一個窗函數。

最內層產生的表長這樣:

會員ID信箱金額
1john@mail.com60,000
1john@mail.com20,000
2mary@mail.com55,000
信箱john@mail.com
金額60,000
信箱john@mail.com
金額20,000
信箱mary@mail.com
金額55,000

中間層的 SELECT * 把這些欄位全部拿出來,再加上一個窗函數算出來的客單價:

會員ID信箱金額客單價
1john@mail.com60,00040,000
1john@mail.com20,00040,000
2mary@mail.com55,00055,000
信箱john@mail.com
金額60,000
客單價40,000
信箱john@mail.com
金額20,000
客單價40,000
信箱mary@mail.com
金額55,000
客單價55,000

既然中間層沒有對欄位做特別的處理,只是「全部拿出來 + 加一個新欄位」,那我們可以直接在最內層就把窗函數加進去:

SELECT 會員.會員ID, 信箱, 金額,
    AVG(金額) OVER (PARTITION BY 會員ID) AS 客單價
FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
WHERE 金額 > 25000

把「挑出欄位」和「計算窗函數」都放在同一個 SELECT 裡面,就不需要分成兩層了。

修改前(兩層巢狀):

SELECT *,
    AVG(金額) OVER (PARTITION BY 會員ID) AS 客單價
FROM (
    SELECT 會員.會員ID, 信箱, 金額
    FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
) AS 會員_訂單_原始
WHERE 金額 > 25000

修改後(合併成一層):

SELECT 會員.會員ID, 信箱, 金額,
    AVG(金額) OVER (PARTITION BY 會員ID) AS 客單價
FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
WHERE 金額 > 25000

因為 JOIN 後面沒有再包子查詢,只是單純的合併,所以不需要額外的別名。

最終的完整查詢

SELECT 姓名, 信箱
FROM (
    SELECT 會員.會員ID, 姓名, 信箱, 金額,
        AVG(金額) OVER (PARTITION BY 會員ID) AS 客單價
    FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID
    WHERE 金額 > 25000
) AS 會員_訂單
WHERE 客單價 > 50000

整個查詢的邏輯很清楚:

  1. FROM 子查詢:把會員和訂單合併,篩選金額 > 25000,計算平均客單價
  2. 外層 WHERE:篩選出客單價超過 5 萬的人
  3. 外層 SELECT:只顯示姓名和信箱

小結

這篇文章很長,帶你走過很多步驟。

為什麼要這樣循序漸進?

因為 JOIN 不是什麼神奇的新東西。

它底層做的事情,跟上一篇的笨方法一模一樣:先排列組合,再用條件篩選。

JOIN 只是把「合併」這件事情,用一個專門的指令包裝起來,讓你寫起來更清楚、更好維護。

這篇真正重要的觀念

1. JOIN ON 會產生一張臨時表單

當你寫 會員 JOIN 訂單 ON ...,SQL 會把兩張表合併起來,產生一張新的表。

這張表可以拿來做後續的操作:篩選、計算、再合併其他表。

理解這一點,你就能看懂複雜的多層查詢在做什麼。

2. ambiguous 問題的本質

合併兩張表的時候,如果兩邊有同名的欄位,資料庫會不知道你要哪一個。

解決方法很簡單:用 SELECT 只挑出你需要的欄位,或是用 AS 幫欄位改名。

這不只是解決報錯,更是一個好習慣——只拿你需要的東西,查詢會更清爽、效能也更好。

3. 語意化讓程式碼更好讀

把合併條件放在 ON,篩選條件放在 WHERE,各司其職。

三個月後回來看,一眼就知道這段程式碼在做什麼。

語法速查

-- 基本語法(JOIN 和 ON 是一組的,不會分開)
表單1 JOIN 表單2 ON 合併條件

-- 合併多張表
FROM 表單A
JOIN 表單B ON A和B的合併條件
JOIN 表單C ON B和C的合併條件

-- 解決欄位重複
SELECT 會員.會員ID, 信箱, 金額
FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員ID

學會 JOIN 之後,你已經掌握了跨表查詢最核心的技能。

接下來的文章會介紹更多 JOIN 的變化型,讓你能處理更複雜的資料合併情境。

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

發表留言

留言將在審核後顯示。

資料庫

目錄

  • 為什麼需要專門的合併指令?
  • 什麼是語意化?
  • 上一篇的問題
  • JOIN 的基本語法
  • 語法結構
  • 跟上一篇的差別
  • JOIN 的好處:合併條件獨立出來
  • JOIN 多張表:一次合併三張以上的表
  • 實作:把逗號寫法改成 JOIN
  • 步驟一:把 FROM 逗號改成 JOIN ON
  • 步驟二:幫合併結果取別名
  • 步驟三:解決 ambiguous 欄位名稱重複問題
  • 解決方法:用 SELECT 只挑出需要的欄位
  • 步驟四:省略不必要的表單名稱
  • 步驟五:簡化巢狀子查詢
  • 最終的完整查詢
  • 小結
  • 這篇真正重要的觀念
  • 語法速查