別再「不確定就用 LEFT JOIN」了

SQL 的 JOIN 語法雖然簡單,但選錯類型會讓資料出現重複或消失,是個容易踩雷的功能。很多人靠「不確定就用 LEFT JOIN」撐過去,但理解各種 JOIN 的真正差異,能省下之後追查異常列數的時間。

這篇文章是以各 JOIN 類型的對照表與結果列數解說為核心的參考資料,最後也會介紹多資料表 JOIN 的寫法,以及如何依 JOIN 類型重新檢視既有的 SQL。

JOIN 種類對照表

users(3 筆:Alice、Bob、Carol)和 orders(Alice 2 筆、Bob 1 筆、Carol 0 筆)為例,比較各 JOIN 傳回的結果。

JOIN 種類傳回的列Alice 與 Bob 的列數Carol(無訂單)的處理僅訂單存在的列
INNER JOIN僅兩邊都有對應的列Alice×2, Bob×1不包含不包含
LEFT JOIN左表全部 + 對應的右表Alice×2, Bob×1Carol 的 1 列(右側為 NULL)不包含
RIGHT JOIN右表全部 + 對應的左表Alice×2, Bob×1不包含若存在則包含(左側為 NULL)
FULL JOIN兩表全部的列Alice×2, Bob×1Carol 的 1 列若存在則包含
CROSS JOIN所有組合(笛卡兒積)3 人 × 3 筆 = 9 列

只有 INNER JOIN 是「篩選」性質,其他所有類型都是朝「保證單邊或雙邊全部列都存在」的方向運作,這個差異正是列數波動的根本原因。

為什麼結果列數會「變多」

這是最容易出包的地方。「JOIN 之後列數比預期多」,十次有九次是因為一對多關係被如實 JOIN 出來的結果

SELECT users.name, orders.id
FROM users
INNER JOIN orders ON users.id = orders.user_id;

即使 users 有 3 筆、orders 有 3 筆,結果的列數會等於 orders 表貢獻的筆數(如果 Alice 有 2 筆訂單,Alice 的列也會出現 2 次)。在做彙總時忽略這點,直接用 COUNT(*),算出來的就不是使用者數,而是訂單數。

-- 錯誤:原本想算使用者數,結果算成「使用者 × 訂單」的列數
SELECT COUNT(*) FROM users INNER JOIN orders ON users.id = orders.user_id;

-- 正確:去除重複後的使用者數
SELECT COUNT(DISTINCT users.id) FROM users INNER JOIN orders ON users.id = orders.user_id;

隨時掌握「JOIN 之前有幾筆」,如果 JOIN 之後的列數不是那個數字的整數倍,就該懷疑是不是 JOIN 條件出了問題——這是實務上的鐵則。

LEFT JOIN 最常踩的坑:WHERE 子句讓 JOIN 失效

明明是為了「想連沒有訂單的使用者也一併列出」才改用 LEFT JOIN,結果一寫 WHERE 條件,馬上又變回跟 INNER JOIN 一樣的結果——這是第二常見的事故。

-- 陷阱:表面上是 LEFT JOIN,但在 WHERE 篩選 orders 會讓 NULL 列消失
SELECT users.name, orders.status
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.status = 'completed';   -- Carol(NULL)在這裡被排除

Carol 沒有訂單,所以 orders.statusNULLWHERE orders.status = 'completed' 會去判斷 NULL = 'completed',而在 SQL 中,任何與 NULL 的比較結果永遠是 NULL(既非真也非假),因此該列會被排除在結果之外。最終效果實質上等同於 INNER JOIN。

解法是把條件寫進 ON 子句

-- 正確:把條件放進 ON 子句,就能保留 LEFT JOIN 的語意
SELECT users.name, orders.status
FROM users
LEFT JOIN orders ON users.id = orders.user_id AND orders.status = 'completed';
-- Carol 的列會保留,orders 那側全部是 NULL

只要記住:想保證左表全部列都在,篩選右側就寫在 ON 子句;篩選左側才寫在 WHERE 子句,就不會搞混了。

多資料表 JOIN:固定一個基準點

要 JOIN 三張以上的資料表而感到困惑時,訣竅是先決定「以哪張表為主詞」

SELECT
  users.name,
  orders.id       AS order_id,
  order_items.quantity,
  products.name   AS product_name
FROM users
LEFT JOIN orders      ON users.id = orders.user_id
LEFT JOIN order_items ON orders.id = order_items.order_id
LEFT JOIN products    ON order_items.product_id = products.id
WHERE users.deleted_at IS NULL;

FROM users 為起點,依序 串接 LEFT JOIN 到訂單、訂單明細、商品。因為全程都以 users 為主詞,「沒有訂單的使用者」和「有訂單但明細資料異常」都能一併找出來(如果中途混入 INNER JOIN,就會在那個節點產生篩選,要特別留意)。

SELF JOIN:用「別名」把一張表當成兩張表

同一張表自己跟自己 JOIN 稱為 SELF JOIN,重點在於用別名(alias)把它當成兩張不同的表來處理。典型例子是「員工與其主管」這種自我參照的結構。

SELECT
  emp.name  AS employee_name,
  mgr.name  AS manager_name
FROM employees emp
LEFT JOIN employees mgr ON emp.manager_id = mgr.id;

employees 分別取別名 empmgr,SQL 上就能寫成「兩張表的 JOIN」。因為通常也想包含沒有主管(manager_id 為 NULL)的員工,所以這裡同樣是 LEFT JOIN 比較自然。

組合與美化 JOIN

如果從零開始寫多資料表 JOIN 很麻煩,可以用 Visual SQL Builder 透過表單組合資料表與 JOIN 條件。切換 JOIN 種類(JOIN/LEFT JOIN/RIGHT JOIN/FULL JOIN)並比較產生的 SQL,很適合搭配這篇的對照表一起確認實際行為。

需要讀懂既有的複雜 JOIN 查詢時,用 SQL 格式化工具 整理過後,會更容易看出哪個 JOIN 對應哪張表。這兩個工具都是完全在瀏覽器內執行,資料表名稱與結構資訊不會外傳。

常見問題

INNER JOIN 和 JOIN 是不一樣的東西嗎?

是一樣的。在大多數 RDBMS 中,JOIN 就是 INNER JOIN 的簡寫。不過為了在程式碼審查時更清楚,許多團隊仍會習慣明確寫出 INNER JOIN

聽說 RIGHT JOIN 很少被使用,是真的嗎?

A RIGHT JOIN B 只要把資料表順序互換,就等同於 B LEFT JOIN A 的結果,因此不少團隊會採用「統一使用 LEFT JOIN、不使用 RIGHT JOIN」的程式碼規範。不過閱讀既有查詢時還是會遇到,理解它的意義仍有必要。

聽說有些資料庫不支援 FULL JOIN?

MySQL 長期以來都不直接支援 FULL JOIN(通常以 UNION 合併 LEFT JOINRIGHT JOIN 來替代)。PostgreSQL 和 SQL Server 則原生支援。使用前請先確認所用資料庫的支援狀況。

貼上 JOIN 條件來確認時,資料會被傳送出去嗎?

不會。Visual SQL BuilderSQL 格式化工具 都完全在瀏覽器內執行,輸入的資料表名稱與查詢內容都不會傳送到伺服器。

總結

  • 只有 INNER JOIN 是「篩選」方向;LEFT/RIGHT/FULL 都是保證單邊或雙邊的全部列
  • 一對多的 JOIN 會讓列數增加,彙總前請考慮使用 COUNT(DISTINCT ...)
  • LEFT JOIN 若要篩選右側請寫在 ON 子句,篩選左側則寫在 WHERE 子句;把右側條件寫進 WHERE 會讓 LEFT JOIN 實質上變成 INNER JOIN
  • 多資料表 JOIN 建議固定一張基準表,並從那裡開始串接 LEFT JOIN
  • SELF JOIN 利用別名讓同一張表看起來像兩張表