上一篇我們學了用逗號合併兩張表,但發現一個問題:合併條件和篩選條件混在一起,很難維護。
這篇來學正式的合併指令:JOIN。
它做的事情跟上一篇一模一樣,只是寫法比較漂亮。
為什麼需要專門的合併指令?
上一篇我們學了用逗號合併兩張表,結果是對的,但寫法有點醜。
你可能會想:「結果對就好啦,幹嘛還要學新的指令?」
這就要講到工程上的一個重要觀念:語意化。
什麼是語意化?
簡單說,就是「寫人話」。
讓你的程式碼一看就知道在做什麼,不用猜。
這樣做的好處是:
- 你自己看得懂
- 你的同事看得懂
- 三個月後的你自己還是看得懂
上一篇的問題
上一篇的寫法:
FROM 會員, 訂單
WHERE 會員.會員ID = 訂單.會員ID乍看之下,不容易一眼看出「這是在做合併」。
WHERE 裡面可能有很多條件,你要仔細看才會發現「喔,原來這一行是合併條件」。
JOIN 的基本語法
用 JOIN 改寫後:
FROM 會員 JOIN 訂單 ON 會員.會員ID = 訂單.會員IDJOIN 這個字在英文是「加入」或「聯合」的意思。
想像兩個人「join hands」(牽手),就是把兩隻手接在一起。
在 SQL 裡面,JOIN 就是把兩張表「接在一起」,也就是合併。
一看到 JOIN 這個字,就知道:這裡在做表的合併。
意圖非常清楚,不用猜。
語法結構
表單1 JOIN 表單2 ON 合併條件JOIN 的意思是「聯合」或「加入」,把兩張表接在一起。
ON 後面放的是合併條件,告訴資料庫「要怎麼接」。
JOIN 和 ON 是一組的,不會分開。
跟上一篇的差別
合併條件從 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 ambiguousambiguous 的意思是「模糊不清」。
為什麼會報這個錯?
因為會員表有「會員ID」,訂單表也有「會員ID」,合併之後,這張臨時表單裡面有兩個「會員ID」。
回憶一下原本的表格:
會員表:
訂單表:
兩張表都有「會員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 * 只是把最內層的欄位全部拿出來,再加上一個窗函數。
最內層產生的表長這樣:
中間層的 SELECT * 把這些欄位全部拿出來,再加上一個窗函數算出來的客單價:
既然中間層沒有對欄位做特別的處理,只是「全部拿出來 + 加一個新欄位」,那我們可以直接在最內層就把窗函數加進去:
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整個查詢的邏輯很清楚:
- FROM 子查詢:把會員和訂單合併,篩選金額 > 25000,計算平均客單價
- 外層 WHERE:篩選出客單價超過 5 萬的人
- 外層 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 的變化型,讓你能處理更複雜的資料合併情境。