半連接把兩個 relation 配起來之後只留下左邊那一份:結果的欄位全部來自左邊,而左邊的每一列至多出現一次。留下的是在右邊找得到對象的那些列。反連接留下的是另一半——左邊那些在右邊配不到任何對象的列,結果的欄位同樣只有左邊的。

SQL 用 EXISTS 與 NOT EXISTS 表達這兩個運算,語言本身沒有為它們保留關鍵字:

1-- 半連接:下過單的顧客
2SELECT 姓名 FROM 顧客
3WHERE EXISTS (SELECT 1 FROM 訂單 WHERE 訂單.顧客編號 = 顧客.顧客編號);
4
5-- 反連接:從未下單的顧客
6SELECT 姓名 FROM 顧客
7WHERE NOT EXISTS (SELECT 1 FROM 訂單 WHERE 訂單.顧客編號 = 顧客.顧客編號);

名字裡的「半」指結果只帶走一邊

三個運算作用在同一對關係上,差別在結果帶走什麼。

運算結果的欄位結果的列數SQL 寫法
連接左右兩邊都有可能超過左邊的列數JOIN ... ON
半連接只有左邊的至多左邊的列數EXISTS、IN
反連接只有左邊的至多左邊的列數NOT EXISTS

右邊那份資料在這兩個運算裡的角色是條件,它的欄位到不了結果。要把右邊的欄位列出來就得用連接,那條判斷標準在 SQL.8 IN、EXISTS 與 JOIN:列數、可取用的欄位與 NULL 的處理。

列數上限省掉去重這一步

半連接問的是配不配得到,所以配到幾筆不影響結果——一位顧客手上有兩張訂單或二十張訂單,EXISTS 都只讓她出現一次。同一個問題用連接寫,她會照訂單張數各出現一次,要補 DISTINCT 把複製出來的列收掉(SQL.6 連接之後的列數與空缺:列數膨脹、外連接補的 NULL 與三值邏輯)。

需要 DISTINCT 因此是一個訊號:這個查詢問的是有沒有,而連接順便做了它不需要的配對。

反連接有兩條描述路徑

NOT EXISTS 直接說「找不到」。外連接那一版先讓左邊每一列都保留下來,再挑出補了 NULL 的那些:

1SELECT 顧客.姓名 FROM 顧客
2LEFT JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號
3WHERE 訂單.訂單編號 IS NULL;

兩段回同一批列,路徑不同——一條由條件直接表達,另一條靠外連接補出來的 NULL 當標記。

NOT IN 的行為與這兩條分岔:子查詢的結果裡出現一個 NULL,整段就回零列且不報錯。反連接的安全寫法因此落在 NOT EXISTS 與外連接這兩條上(SQL.6 連接之後的列數與空缺:列數膨脹、外連接補的 NULL 與三值邏輯)。

概念位置

半連接與反連接跟連接一樣是導出運算:半連接等於連接之後只投影左邊的欄位再去重,反連接等於左邊減去半連接的結果。關聯代數的正典運算清單因此多半只列到連接,而「有沒有對應資料」這一族問題全部落在這兩個運算上——找出下過單的顧客、找出從未下單的顧客、找出比前一天熱的日子,題目與寫法各異而運算只有這兩個。

計畫上看不看得到這兩個運算的名字,各家不同。DuckDB 1.5.5 把它印成 Join Type: SEMI;SQLite 3.51.0 沒有對應的運算子,它把 EXISTS 的子查詢編成帶一個 LIMIT 1 計數器的迴圈,找到第一筆就跳出去——語意相同,而執行計畫上只看得到一個子查詢節點。

它們與積分別是連接結果大小的兩個極端。積是條件缺席時的上限,列數是兩邊相乘;半連接把上限壓到左邊的列數,因為右邊配到幾列都只算一次。中間那一段是連接,結果的大小由配對的重複程度決定。

往下走

IN、EXISTS 與 JOIN 在列數與可取用欄位上的差別、以及選哪一種只問一句話,在 SQL.8 IN、EXISTS 與 JOIN:列數、可取用的欄位與 NULL 的處理。從一句業務描述推到反連接、以及為什麼計數那條路走不通,在 SQL.9 分組鍵的選擇:鍵與一組所代表的對象。同一張表的兩列互相比較時這兩個運算長什麼樣,在 SQL.7 表的出現與別名:表名的指稱、別名的作用範圍與自連接。同一組寫法的快慢隨索引與資料分布重排的實測,在 SQL.17 查詢的代價。