「找出有對應資料的那些」這類問題有 IN、EXISTS 與 JOIN 三種常見寫法,而它們在很多情況下回同樣的答案。這種巧合讓它們看起來可以互換,而三者描述的是不同的事,回的列數也不一定相同。

分開它們的問題只有一個:除了判斷有沒有,還要不要用到被比對的那張表的欄位。

同一題用 IN、EXISTS 與 JOIN 各寫一次

書店的三位顧客裡只有佳穎下過單,兩張都是她的。要找出下過單的顧客。

IN 問的是成員資格——這個顧客編號在不在括號裡那個子查詢交出的清單裡:

1SELECT 姓名 FROM 顧客 WHERE 顧客編號 IN (SELECT 顧客編號 FROM 訂單);
2-- 佳穎

EXISTS 問的是存在性——有沒有至少一張訂單屬於這位顧客:

1SELECT 姓名 FROM 顧客
2WHERE EXISTS (SELECT 1 FROM 訂單 WHERE 訂單.顧客編號 = 顧客.顧客編號);
3-- 佳穎

JOIN 做的是另一件事——把兩張表併成一個新的關係:

1SELECT 顧客.姓名 FROM 顧客 JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號;
2-- 佳穎、佳穎

同一個「找出下過單的顧客」的問題,IN 與 EXISTS 回一列而 JOIN 回兩列——佳穎在 JOIN 的結果裡出現了兩次。

列數:條件與連接的差別

IN 與 EXISTS 出現在 WHERE 裡,它們是條件。條件對每一列只做一件事——留下或丟掉——所以顧客表有三列,過完條件最多剩三列,一列都不會多出來。

JOIN 出現在 FROM 裡,它參與的是關係的組成。佳穎配得上兩張訂單,於是她在新的關係裡出現兩次(SQL.6 連接之後的列數與空缺:列數膨脹、外連接補的 NULL 與三值邏輯)。加上 DISTINCT 之後結果才與 IN、EXISTS 一致:

1SELECT DISTINCT 顧客.姓名 FROM 顧客 JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號;
2-- 佳穎

需要 DISTINCT 這件事本身是一個訊號:它表示這個查詢只是要判斷有沒有,而 JOIN 順便做了不需要的配對。這種時候 IN 或 EXISTS 更貼合意圖,因為它們一開始就沒有把列複製出來。

IN 與 EXISTS 做的運算在關聯代數裡叫半連接:顧客表與訂單表配對之後只留下顧客表的列,每一列至多出現一次。否定式的 NOT EXISTS 留下顧客表裡配不到任何訂單的列,叫反連接(Semi-join 與 Anti-join(半連接與反連接))。

可取用的欄位:被比對的那張表在不在 FROM 裡

要列出每位顧客的訂單金額,JOIN 是最直接的一條路:

1SELECT 顧客.姓名, 訂單.金額 FROM 顧客 JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號;
2-- 佳穎 300 / 佳穎 500

同一件事用 IN 寫不出來:

1SELECT 姓名, 訂單.金額 FROM 顧客 WHERE 顧客編號 IN (SELECT 顧客編號 FROM 訂單);
2-- SQLite: no such column: 訂單.金額
3-- DuckDB: Referenced table "訂單" not found!

錯誤訊息說得很直接:訂單 這張表不在 FROM 裡面。子查詢是一個獨立的查詢,它算完之後只交出一份值的清單,而訂單表沒有成為外層 FROM 的一部分——外層因此引用不到它的任何欄位。

帶欄位出來還有另一條路——把子查詢寫在 SELECT 裡並讓它引用外層的欄位。引用外層欄位的子查詢叫相關子查詢(correlated subquery):

1SELECT 姓名, (SELECT 金額 FROM 訂單 WHERE 訂單.顧客編號 = 顧客.顧客編號
2              ORDER BY 訂單編號 LIMIT 1) FROM 顧客;
3-- 佳穎 300 / 宗翰 NULL / 雅文 NULL

它一次只交得出一個值,所以要多欄或多列時仍然回到 JOIN;而它的執行方式是逐列的,代價怎麼算見 SQL.17 查詢的代價:資料分布、索引與寫法各自的影響。

這時候佳穎出現兩次是對的,因為問的是每一張訂單的金額,而她確實有兩張。這一題本來就要每張訂單各佔一列,所以列數變多正是這個寫法要交出來的東西。

選用的判斷標準:要不要用到被比對那張表的欄位

要不要用到被比對的那張表的欄位。 要用到就得 JOIN,因為只有它把那張表帶進 FROM。這時候要順帶檢查列數是不是預期內的——後面如果接聚合,sum 與 count 算的是配對後的列;這個膨脹怎麼發生、可加與不可加的聚合各自怎麼修,在 SQL.6 連接之後的列數與空缺。

只是判斷有沒有,就用 IN 或 EXISTS。它們是條件,對每一列只做留或丟,寫出來的意圖比 JOIN 加 DISTINCT 清楚——而哪一種比較快是另一個問題,答案取決於資料與索引。SQL.17 查詢的代價 用一道重複值的題目量分組、自連接與 EXISTS 三種寫法,並示範加一個索引之後排名重排——EXISTS 從最慢掉到與分組同一個量級。

IN 與 EXISTS 的分岔:子查詢含 NULL 時的否定式

IN 與 EXISTS 在「有沒有」這件事上同值,而在一個地方分岔:子查詢的結果裡有 NULL 的時候。

清單裡只要混了一個 NULL,NOT IN 就再也不會為真:值在清單裡的列判成明確的假,值不在清單裡的列因為那個 NULL 判成未知,兩種都被 WHERE 丟掉,整段查詢回零列且不報錯;NOT EXISTS 不受影響。在共用資料上多一張沒填顧客編號的訂單就看得到:

1INSERT INTO 訂單 VALUES (103, NULL, '2026-03-20', 100);   -- 訂單 103 沒有填顧客編號
2
3SELECT 姓名 FROM 顧客 WHERE 顧客編號 NOT IN (SELECT 顧客編號 FROM 訂單);
4-- 共用資料:宗翰、雅文
5-- 多了訂單 103 之後:零列,不報錯(子查詢交出的清單是 1、1、NULL)
6
7SELECT 姓名 FROM 顧客
8WHERE NOT EXISTS (SELECT 1 FROM 訂單 WHERE 訂單.顧客編號 = 顧客.顧客編號);
9-- 共用資料與多了訂單 103 之後都是:宗翰、雅文

完整的推導在 SQL.6 連接之後的列數與空缺:列數膨脹、外連接補的 NULL 與三值邏輯,包括 NOT EXISTS 的安全範圍為什麼限定在等號配對。零列與「確實沒有」在結果上分不開,所以 NOT IN 碰到 NULL 的錯只能靠寫的人預先知道「子查詢有 NULL 時 NOT IN 會失效」那條規則——SQL.13 查詢的合法性與答案的正確性:引擎檢查的範圍、答案錯掉的成因與查證方法 把它歸成誤解語意模型:寫的人對某條規則的預期與語意模型的規定不同,而誤解語意模型修得掉,改對之後永遠對。

所以肯定式的 IN 與 EXISTS 可以按可讀性挑,而否定式一律用 NOT EXISTS——除非能保證子查詢那一欄不會有 NULL,而那個保證要來自約束而不是來自習慣:翻遍現在的資料都沒有 NULL 只證明此刻沒有,而查詢要活得比這一批資料久。

還有一個形態上的差別:本篇 IN 的子查詢與外層無關,可以單獨拿出來執行,叫獨立子查詢;EXISTS 的子查詢引用了外層的欄位,離開外層就跑不動,是相關子查詢。讀陌生的查詢時,看子查詢裡有沒有出現外層的表名,就分得出它是算一次的獨立子查詢,還是對外層每一列各問一次的相關子查詢。

「語意相同就按可讀性挑」這個處置的適用範圍不只 IN、EXISTS 與 JOIN 這三種寫法:哪幾類寫法差異是免費的、哪一類會與效能分岔,在 SQL.21 可讀性與執行代價:兩者一致與分岔的情形。