FROM 走完之後得到的 relation 已經不是任何一張來源表。它的列數可能變多,而它多出來一些來源表裡沒有的空缺。後面每一個子句都在這個新的關係上操作。本篇從這個關係已經成形的那一刻開始;它是逐個 JOIN 累積出來的,每個 JOIN 的左運算元是什麼在 SQL.4 鏈式連接的左運算元。

列數變多與多出空缺各自帶來一類錯誤,兩類都不報錯。列數變多帶來的錯誤本篇叫列數膨脹:聚合數的是連接之後的列,回了比實際大的數字。空缺帶來的錯誤本篇叫比較 NULL 的錯誤:拿 NULL 做比較,答案是未知而不是真或假,WHERE 裡的等號與 NOT IN 因此回了零列,NOT EXISTS 子查詢裡的不等號則多留了一列。

列數膨脹與各類聚合的修法

這一節反覆數的是列,也就是橫向的一筆資料——三張訂單就是三列,欄位有幾個與這件事無關。

書店的訂單表有兩列,而共用資料裡兩張訂單只有 101 被評價過,所以接上評價表之後仍是兩列。替 101 再加一則評價,列數就變成三:

1SELECT count(*) FROM 訂單 LEFT JOIN 評價 ON 評價.訂單編號 = 訂單.訂單編號;
2-- 共用資料:2
3
4INSERT INTO 評價 VALUES (9002, 101, 3);
5
6SELECT count(*) FROM 訂單 LEFT JOIN 評價 ON 評價.訂單編號 = 訂單.訂單編號;
7-- 替 101 再加一則評價之後:3

訂單一張都沒有增加,而連接後的關係多了一列。原因是配對是逐列做的:訂單 101 配得上兩則評價,於是它在結果裡出現兩次,兩次帶著不同的評價。

後續的聚合算的是這個新關係的列,不是訂單表的列。 所以數佳穎有幾張訂單,直接數會得到錯的答案:

1SELECT 顧客.姓名, count(*) AS 看起來的訂單數,
2       count(DISTINCT 訂單.訂單編號) AS 實際訂單數
3FROM 顧客 JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號
4LEFT JOIN 評價 ON 評價.訂單編號 = 訂單.訂單編號
5GROUP BY 顧客.姓名;
6-- 佳穎 | 3 | 2

佳穎實際有兩張訂單,而 count(*) 回報三。SQLite、DuckDB 與 PostgreSQL 的答案都是這一組。

列數膨脹最難察覺的地方在於數字看起來合理。count(*) 的三比實際訂單數的二只多一,而且它會隨著評價增加而慢慢變大——沒有任何一刻它會大到讓人起疑。

判斷方法是問這個聚合要數的是哪一個層次的單位——訂單、還是連接之後的列。決定那個層次的是分組鍵(選鍵的判斷標準在 SQL.9 分組鍵的選擇:鍵與一組所代表的對象),而把單位拉回來的做法依聚合的性質分三種。

數個數的用 count(DISTINCT 訂單編號),計數的單位就回到訂單。

加總的不能用 DISTINCT。直接加總時同一張訂單的金額被加了兩次,而 sum(DISTINCT 金額) 去掉的是重複的值、不是重複的訂單,兩張金額剛好相同的訂單會被它當成同一筆而少加一次。加總的解法是讓展開的那一層在接上來之前就已經收成一列:

 1-- 直接連接再加總:101 配到兩則評價,300 被加了兩次
 2SELECT sum(訂單.金額) FROM 訂單 LEFT JOIN 評價 ON 評價.訂單編號 = 訂單.訂單編號;
 3-- 1100
 4
 5-- 括號裡那一段先把每張訂單的評價收成一列,接上來之後每張訂單在結果裡只有一列
 6SELECT 訂單.訂單編號, 訂單.金額, 評價數
 7FROM 訂單
 8LEFT JOIN (SELECT 訂單編號, count(*) AS 評價數 FROM 評價 GROUP BY 訂單編號) AS 評價統計
 9  ON 評價統計.訂單編號 = 訂單.訂單編號;
10-- 101 | 300 | 2
11-- 102 | 500 |

每張訂單在這份結果裡只有一列,對它加總金額時每張訂單也就只算一次。

不可加的(平均、中位數、百分位)不能先在每張訂單這一層算完,再拿到更粗的那一層合併。先算每張訂單的平均星等、再把這些平均值平均一次,得到的是「平均的平均」:評價則數多的訂單與只有一則的訂單權重一樣,結果就偏了。DISTINCT 在這裡也沒有幫助,它去掉的是重複的值,不是重複的列。再替訂單 102 加一則一星評價,讓兩張訂單的則數不同:

1INSERT INTO 評價 VALUES (9003, 102, 1);
2
3-- 平均的平均:101 的兩則平均 4、102 的一則平均 1,再平均一次
4SELECT avg(每單平均) FROM (SELECT 訂單編號, avg(星等) AS 每單平均 FROM 評價 GROUP BY 訂單編號);
5-- 2.5

佳穎收到的三則評價是 5、3、1,實際的平均是 3。平均與中位數、百分位的出路不同。

平均可以拆成兩個可加的量。 子查詢裡同時算 sum 與 count,接上來之後各自加總,最後才相除:

1SELECT 顧客.姓名, sum(評價統計.星等總和) * 1.0 / sum(評價統計.則數) AS 平均星等
2FROM 顧客
3JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號
4JOIN (SELECT 訂單編號, sum(星等) AS 星等總和, count(*) AS 則數 FROM 評價 GROUP BY 訂單編號) AS 評價統計
5  ON 評價統計.訂單編號 = 訂單.訂單編號
6GROUP BY 顧客.姓名;
7-- 佳穎 | 3.0

sum 與 count 在每一層都可以直接相加,所以不管中間經過幾層彙總,最後相除的都是全部評價的總和與總則數。乘上 1.0 是為了避免兩個整數相除時被截成整數。

中位數與百分位拆不成可加的量。 知道兩張訂單各自的中位數,推不出兩張合起來的中位數,所以它們只能在目標層次上,直接對原始的列計算,而且每一筆資料在那個集合裡只能出現一次。算「每位顧客收到的評價星等中位數」時,要計算的集合是評價的列,而顧客接訂單再接評價,每一則評價正好出現一次,這個集合可以直接用;如果連接裡還接了會讓評價重複的表,就要先把評價收成「每則一列」再算。計算本身各家的寫法不同。SQLite 的 median() 與 percentile() 屬於可選的擴充,手上那一份有沒有,跑一次 SELECT median(1); 就知道;沒有的話用視窗函數替每位顧客的評價排序,取中間那一列,則數是偶數時取中間兩列的平均:

 1-- 此時佳穎收到的評價是 5、3、1
 2-- PostgreSQL:percentile_cont 在每一組裡依 ORDER BY 排好之後取 0.5 分位
 3SELECT 顧客.姓名, percentile_cont(0.5) WITHIN GROUP (ORDER BY 評價.星等) AS 中位數
 4FROM 顧客
 5JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號
 6JOIN 評價 ON 評價.訂單編號 = 訂單.訂單編號
 7GROUP BY 顧客.姓名;
 8-- 佳穎 | 3
 9
10-- SQLite 沒有 median() 時:名次是那位顧客的評價依星等排序後的位置,則數是那位顧客的評價總數
11WITH 排序後 AS (
12  SELECT 顧客.姓名, 評價.星等,
13         row_number() OVER (PARTITION BY 顧客.顧客編號 ORDER BY 評價.星等) AS 名次,
14         count(*)     OVER (PARTITION BY 顧客.顧客編號) AS 則數
15  FROM 顧客
16  JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號
17  JOIN 評價 ON 評價.訂單編號 = 訂單.訂單編號
18)
19SELECT 姓名, avg(星等) AS 中位數 FROM 排序後
20WHERE 名次 IN ((則數 + 1) / 2, (則數 + 2) / 2)   -- 則數是奇數時兩個值相同,偶數時是中間兩列
21GROUP BY 姓名;
22-- 佳穎 | 3.0

這一段的例子做完之後把 9003 刪掉:DELETE FROM 評價 WHERE 評價編號 = 9003;。下一節要用到訂單 102 沒有評價的狀態。

外連接補出來的 NULL:count(*) 與 count(欄位) 的差別

外連接配不上時補的 NULL,來源表裡並不存在。LEFT / RIGHT / FULL 各自保護哪一側、因此在哪一側補空缺,定義在 Outer Join(外連接)。count 對它的處理與一般的值不同:

1SELECT count(*) AS 全部列, count(評價.星等) AS 有星等的列
2FROM 訂單 LEFT JOIN 評價 ON 評價.訂單編號 = 訂單.訂單編號;
3-- 3 | 2

count(*) 數的是列,count(欄位) 數的是那一欄有值的列。差額就是空缺的數量。這個差別在沒有連接的查詢裡很少現形,因為原始資料多半沒那麼多空值。

NULL 的比較結果與 IS NULL

要找出沒下過單的顧客,很自然會寫成「訂單編號等於 NULL」:

 1SELECT 顧客.姓名 FROM 顧客
 2LEFT JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號
 3WHERE 訂單.訂單編號 = NULL;
 4-- 0 列,而且沒有任何錯誤訊息
 5
 6-- 同一段改成 IS NULL
 7SELECT 顧客.姓名 FROM 顧客
 8LEFT JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號
 9WHERE 訂單.訂單編號 IS NULL;
10-- 宗翰、雅文

= NULL 那一段在三個引擎上都回零列、都不報錯。這個 IS NULL 要寫在 WHERE 而不是 ON,理由是它描述的是連接完成之後的那份結果;同一個條件放兩邊為什麼在外連接底下留下不同的列,在 SQL.5 ON 與 WHERE:連接條件與篩選條件各自的職責。

原因是 NULL 表示「這裡沒有值」,而拿一個未知的值去比對任何值,答案也是未知:

1SELECT (NULL = NULL), (NULL IS NULL);
2-- NULL | 1        SQLite 用 1 表示真

NULL = NULL 的結果既不是真也不是假,它是第三種答案。而 WHERE 只留下判斷為真的列,所以「未知」與「假」在這一步的下場相同——列被丟掉。

IS NULL 是另一個運算子,它問的是「這個位置有沒有值」,答案只有真或假兩種。判斷空缺一律用 IS NULL 與 IS NOT NULL,比較運算子在這件事上永遠回零列。

子查詢含 NULL 時的 NOT IN 與 NOT EXISTS

同一套三值邏輯(見 NULL)製造了一個更難發現的形態。書店要找出沒下過單的顧客,改用 NOT IN:

1SELECT 姓名 FROM 顧客 WHERE 顧客編號 NOT IN (SELECT 顧客編號 FROM 訂單);
2-- 共用資料:宗翰、雅文

答案正確。接著訂單表多了一列顧客編號是 NULL 的資料——比如一筆還沒綁定顧客的訂單。同一段查詢再跑一次,三個引擎都是零列,仍然不報錯:

1INSERT INTO 訂單 VALUES (103, NULL, '2026-03-10', 200);
2
3SELECT 姓名 FROM 顧客 WHERE 顧客編號 NOT IN (SELECT 顧客編號 FROM 訂單);
4-- 0 列
5--   子查詢交出 1、1、NULL,NOT IN 對宗翰(顧客編號 2)展開成 2 <> 1 AND 2 <> 1 AND 2 <> NULL
6--   與 1 比較的兩項為真,與 NULL 比較的那一項是未知,AND 的結果是未知,WHERE 不留

NOT IN 展開成一串「不等於」的 AND,與 NULL 比較的那一項答案是未知,整串的結果因此永遠不會是真。子查詢裡出現任何一個 NULL,NOT IN 就對每一列都回答「未知」。

改成 NOT EXISTS 不受影響:

1SELECT 姓名 FROM 顧客
2WHERE NOT EXISTS (SELECT 1 FROM 訂單 WHERE 訂單.顧客編號 = 顧客.顧客編號);
3-- 共用資料:宗翰、雅文
4-- 訂單 103 進來之後:宗翰、雅文
5
6-- 刪掉 103:留著的話,金額最高的那段查詢會把它也選進來
7DELETE FROM 訂單 WHERE 訂單編號 = 103;

NOT EXISTS 問的是「有沒有配得上的列」,那是一個關於存在性的問題,答案只有真假兩種。

這個安全範圍限定在子查詢用等號配對的時候。 子查詢裡改用不等號比較的話,NULL 一樣會讓結果出錯。「找出每位顧客金額最高的訂單」可以寫成「同一位顧客沒有另一張金額更高的訂單」,而金額為 NULL 的那張訂單會被選成「最高」。原因不是 NULL 比較大:NULL 沒有進到 NOT EXISTS 的答案裡,它進到了子查詢的比較條件裡,比較的答案是未知而不是真,子查詢因此一列都找不到,NOT EXISTS 於是成立。在共用資料上加一張金額未填的訂單 104 就看得到:

 1INSERT INTO 訂單 VALUES (104, 1, '2026-03-12', NULL);
 2
 3SELECT a.訂單編號, a.金額 FROM 訂單 a
 4WHERE NOT EXISTS (SELECT 1 FROM 訂單 b WHERE b.顧客編號 = a.顧客編號 AND b.金額 > a.金額);
 5-- 102 | 500    子查詢對 102 找不到金額更高的訂單
 6-- 104 |        子查詢比的是 b.金額 > NULL,對每一張 b 都是未知,一列都找不到
 7--   101 沒有出現:102 的 500 > 300 為真,子查詢找得到一列
 8
 9-- 子查詢之外另寫 a.金額 IS NOT NULL,排除金額未填的那一張
10SELECT a.訂單編號, a.金額 FROM 訂單 a
11WHERE a.金額 IS NOT NULL
12  AND NOT EXISTS (SELECT 1 FROM 訂單 b WHERE b.顧客編號 = a.顧客編號 AND b.金額 > a.金額);
13-- 102 | 500
14
15DELETE FROM 訂單 WHERE 訂單編號 = 104;

佳穎的「金額最高的訂單」在沒有 IS NOT NULL 的那段查詢裡回了兩張:真正最高的 102,以及金額未填的 104。

列數改變、空缺與 NULL 比較的共同來源

列數變了、空缺出現了、空缺的比較是第三種答案——三者都來自同一件事:FROM 產出的是一個新的關係,而後續每一步都在它上面操作。 拿來源表的直覺去讀後面的子句,就會在這三個地方各撞一次。

列數會變與空缺會出現,在本篇是給定的,而兩者各自在更早的時候就決定了。列數會不會膨脹,由常一起取的資料切在幾張表決定;空缺存不存在,由這一欄允不允許為空決定——兩個都是建表時下的,下的當下看不出代價。backend 1.16 Schema 設計決定的查詢代價 各給一節,並量出雙重展開讓聚合翻倍,以及 DISTINCT 這個修法對不對由資料決定。

列數膨脹與比較 NULL 的錯誤各有各的機制,而共通的是引擎對它們都沒有回報的管道:列數膨脹的結果列數與欄位看不出異常,而計數與總額已經多算;等號比 NULL 與 NOT IN 回零列,而零列與「確實沒有」分不開;NOT EXISTS 子查詢裡的不等號多留了金額未填的那張訂單,多出來的那一列也是一張真實存在的訂單。SQL.13 查詢的合法性與答案的正確性:引擎檢查的範圍、答案錯掉的成因與查證方法 拿列數膨脹當例子,說明結果的形狀正確為什麼不構成保證。