SQL.7 表的出現與別名:表名的指稱、別名的作用範圍與自連接
FROM 裡寫下的表名指的是那張表在這個查詢裡的一次出現,而那個名字是這次出現的稱呼。標準把這件事叫 table reference,關聯演算(relational calculus)裡的傳統名稱是 range variable,各家文件裡也常寫成 correlation name——三個名字說的是同一件事。同一張表因此可以出現兩次、各自扮演不同角色——這就是自連接。
一張表只出現一次的時候,這個區別沒有作用。出現兩次就有:兩次出現共用同一個名字,引擎分不出某個欄位屬於哪一次,於是擋下來。
引擎把這件事講得很明白,而擋下來的時機分兩種。PostgreSQL 與 DuckDB 看到重複的名字就停,訊息分別是 table name "訂單" specified more than once 與 Duplicate alias "訂單" in query!;SQLite 讓查詢往下走,等到某個欄位引用無從分辨時才回 ambiguous column name。表名本身就是預設的別名,所以同一張表的兩次出現共用同一個名字時,等於兩次出現叫同一個稱呼,之後每一次引用都無從分辨。
兩張表共用欄名時的欄位歸屬
欄位的歸屬要靠名字決定,所以兩張不同的表共用一個欄名時同樣分不開。書店的顧客表與訂單表都有 顧客編號:
1SELECT 顧客編號 FROM 顧客 LEFT JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號;
2-- MySQL: ERROR 1052: Column '顧客編號' in field list is ambiguous
3-- SQLite: ambiguous column name: 顧客編號
4-- DuckDB: Ambiguous reference to column name "顧客編號"id 是這件事最常撞到的欄位名,因為多數表都有一個。連接之後每個欄位引用都要能追回它屬於哪一次出現,加上表名或別名當前綴就解決。
自連接只是這個規則的極端情形:兩次出現連表名都一樣,所以連前綴都救不了,非取別名不可。
表一多,要判讀的就變成每個 ON 引用得到哪些出現——寫在它上面的那些出現都已經併進左運算元,寫在它下面的還沒進來。SQL.4 鏈式連接的左運算元 寫左運算元隨著往下走而變大的過程。
用別名分開同一張表的兩次出現
訂單表裡的兩張單都掛在佳穎名下。要找出同一位顧客的訂單先後配對——哪一張在前、哪一張在後。這件事需要把訂單表的兩列擺在一起比較,而一列一列處理的查詢做不到,除非讓訂單表出現兩次:
1SELECT 早.訂單編號, 早.下單日, 晚.訂單編號, 晚.下單日
2FROM 訂單 AS 早
3JOIN 訂單 AS 晚 ON 早.顧客編號 = 晚.顧客編號 AND 早.下單日 < 晚.下單日;
4-- 101 | 2026-03-02 | 102 | 2026-03-09早 與 晚 是同一張表的兩次出現。連接條件在做兩件事:顧客編號 相同把配對限制在同一個人身上,下單日 的大小關係決定哪一次出現扮演「早」、哪一次扮演「晚」。
條件裡的第二項是這個寫法的重點。 少了它,每張訂單會跟同一位顧客的每張訂單配對一次,包括跟自己配。加上嚴格的大小比較之後,每一對只留下一個方向,而同一張訂單跟它自己組成的那一對,因為兩邊日期相等也被排除。同一位顧客有三張訂單時,每兩張各配一次,不多不少:
1-- 拿掉日期的比較,只留顧客編號相同
2SELECT 早.訂單編號, 晚.訂單編號
3FROM 訂單 AS 早
4JOIN 訂單 AS 晚 ON 早.顧客編號 = 晚.顧客編號;
5-- 共用資料:101|101、101|102、102|101、102|102(兩個方向各一次,加上各自跟自己配)
6
7-- 日期的比較放回去
8SELECT 早.訂單編號, 晚.訂單編號
9FROM 訂單 AS 早
10JOIN 訂單 AS 晚 ON 早.顧客編號 = 晚.顧客編號 AND 早.下單日 < 晚.下單日;
11-- 共用資料:101|102
12-- 佳穎多一張 103(下單日 2026-03-15)之後:101|102、101|103、102|103自連接的適用問題:前後配對、同組比較與層級關係
自連接處理的是同一批資料裡兩筆資料之間的關係——兩列,不是兩個欄位。一列一列的處理看不到這種關係,因為每一列在被判斷的時候不知道其他列長什麼樣子。
自連接常見的形態有三種。前後配對:同一位顧客的訂單先後配對,把時間上相鄰或有先後關係的兩列擺在一起。同組比較:找出訂單金額高於同一位顧客其他訂單的那些,比較的對象是同一張表裡的兄弟列。層級關係:一張員工表裡每個人有一個主管欄位指向同一張表的另一列,要把人與他的主管一起列出來時,員工表出現兩次,一次當員工、一次當主管。
前後配對、同組比較與層級關係三種的共同點是要比較的兩列住在同一張表裡。需不需要自連接由兩個獨立的問題決定:這個問題要不要拿一列去跟同一張表的另一列比——不比的問題不屬於這一族;以及要不要把另一列的欄位輸出出來——這個問題決定族裡選哪一種寫法。
要不要輸出另一列的欄位,就是 SQL.8 IN、EXISTS 與 JOIN:列數、可取用的欄位與 NULL 的處理 那條判斷標準——自連接也是一種 JOIN,所以它同樣適用。只是判斷「有沒有另一列符合條件」而不輸出它,用 EXISTS 比自連接省一次配對展開;要把兩列並排列出來(像上面的訂單先後配對),才非自連接不可。
省下的量由配對會不會膨脹決定,所以這條判斷標準要連著連接鍵唯不唯一一起看。 連接鍵在對面那一次出現裡唯一的時候,一列只配得到一列,兩種寫法產出的列數相同,EXISTS 省下的是零——那時選哪一個回到讀起來哪一個貼合意圖。一列配得到多列的時候展開才會發生,而那正是 SQL.6 連接之後的列數與空缺:列數膨脹、外連接補的 NULL 與三值邏輯 寫的列數膨脹。判斷的動作是問這個連接鍵在對面那一次出現裡唯不唯一,而唯一與否要由約束回答,不由現在這批資料回答。
同組比較最常見的題型是每組最大值,而它套用「要不要輸出另一列的欄位」這條判斷標準時,答案是不輸出。 「找出每位顧客金額最高的訂單」輸出的是那張訂單自己的欄位,同顧客的其他訂單只拿來比大小,所以寫成 NOT EXISTS 就夠。前後配對的「找出每筆訂單與同顧客下一筆的差額」則要用到另一列的金額,那就非自連接不可。
每組最大值的條件是比大小,而比大小自己會帶出兩個邊界:並列的最大值與可為空的比較欄。NOT EXISTS 會把並列的那幾筆全部留下,要只留一筆得另外加一個決勝的條件,通常取編號最小的那筆。NULL 跟誰比都判不成真,所以「同顧客裡沒有比它大的」對金額是 NULL 的那一張恆成立,它會被選成最大;要排除它,IS NOT NULL 得寫在外層查詢——子查詢裡的比較對它本來就判不成真,寫在那裡攔不住(機制見 SQL.6 連接之後的列數與空缺:列數膨脹、外連接補的 NULL 與三值邏輯)。
1-- 每位顧客金額最高的訂單:同顧客裡找不到比它大的那一張
2SELECT o.訂單編號, o.金額
3FROM 訂單 o
4WHERE NOT EXISTS (
5 SELECT 1 FROM 訂單 i
6 WHERE i.顧客編號 = o.顧客編號 AND i.金額 > o.金額
7);
8-- 共用資料:102 | 500
9-- 佳穎多一張 103(金額 500)之後:102 | 500、103 | 500(並列的兩張都留下)
10-- 佳穎多一張 104(金額 NULL)之後:102 | 500、104 | NULL(NULL 那一張被選成最大)
11
12-- 決勝的條件:金額相同時,編號較小的那一張也算「比它大」
13SELECT o.訂單編號, o.金額
14FROM 訂單 o
15WHERE NOT EXISTS (
16 SELECT 1 FROM 訂單 i
17 WHERE i.顧客編號 = o.顧客編號
18 AND (i.金額 > o.金額 OR (i.金額 = o.金額 AND i.訂單編號 < o.訂單編號))
19);
20-- 佳穎多一張 103(金額 500)之後:102 | 500
21
22-- 排除金額是 NULL 的那一張:條件寫在外層
23SELECT o.訂單編號, o.金額
24FROM 訂單 o
25WHERE o.金額 IS NOT NULL
26 AND NOT EXISTS (
27 SELECT 1 FROM 訂單 i
28 WHERE i.顧客編號 = o.顧客編號 AND i.金額 > o.金額
29 );
30-- 佳穎多一張 104(金額 NULL)之後:102 | 500
31-- 同一句改寫進子查詢(AND i.金額 IS NOT NULL):仍回 102 | 500、104 | NULL表只出現一次時別名的作用
同一張表只出現一次的時候,別名的作用是縮短欄位引用。長表名在有三四張表的查詢裡會讓每個欄位引用都變得很長,取一個短別名之後可讀性差很多。
而作用不只是省字。取了別名之後原本的表名在這個查詢裡就不再是它的稱呼,三個引擎都會擋下來:PostgreSQL 回 invalid reference to FROM-clause entry for table "訂單" 並附上一句提示要改用別名,SQLite 回 no such column,DuckDB 回 Referenced table "訂單" not found。FROM 訂單 AS 早 宣告的是「這次出現叫早」,而宣告過後就只有這一個稱呼。
別名的取名:標示這次出現扮演的角色
自連接的兩次出現在問題裡扮演不同角色,而別名是查詢裡唯一標示角色的地方。同一位顧客的訂單先後配對:
1SELECT 早.訂單編號, 晚.訂單編號, 晚.金額 - 早.金額
2FROM 訂單 早 JOIN 訂單 晚
3 ON 晚.顧客編號 = 早.顧客編號 AND 晚.下單日 > 早.下單日;把兩個別名換成 a 與 b,查詢的行為一模一樣,而 b.金額 - a.金額 這一行要回頭看 ON 才知道哪一個是後來那張單。判斷標準落在這裡:讀到用它的那一行時,方向自己說得出來。 名字取得對的時候,晚.金額 - 早.金額 讀起來就是它的意思。
引擎對這個名字沒有意見。把兩個別名對調,條件也跟著對調:
1FROM 訂單 晚 JOIN 訂單 早
2 ON 早.顧客編號 = 晚.顧客編號 AND 早.下單日 > 晚.下單日叫「早」的那一次出現拿到的是後來那張單。查詢照跑,結果與上面那一段相同,而每一個名字都指反了。別名因此也是一種宣告——它說明這次出現代表什麼,兌現與否由寫的人負責。
表的別名與欄位別名各自可用的時機
SELECT 裡的 AS 替算出來的欄位取名,FROM 裡的 AS 替表的一次出現取名。兩者的作用範圍不同,可用的時機也不同。
表的別名在 FROM 那一步就成立,所以 WHERE、GROUP BY、ON 全部用得到它。欄位別名到 SELECT 那一步才誕生,所以 WHERE 照語意模型用不到。兩個 AS 長得一樣而落在求值順序的兩端。
某些引擎讓 WHERE 用得到欄位別名,那是引擎的放寬,不是模型的規定。SQL.2 子句的求值順序 走過每一步能用到哪些欄位與值,並把模型層擋得住的限制與各家引擎自己放寬的分開——表的別名不在它的範圍裡,因為別名在 FROM 那一步就成立了。