SQL.4 鏈式連接的左運算元:累積方式與外連接的保護範圍
FROM 子句由上往下把表併進來,每一個 JOIN 的左運算元是它上面全部合起來的結果。第一個 JOIN 的左邊剛好是一張表,從第二個開始就不是了。
左運算元會累積這件事決定三件事:每個 ON 引用得到哪些欄位、每個 LEFT 保護的是哪些列(保護指 LEFT 左邊配不到對象的列也留在結果裡、右邊那幾欄補 NULL)、以及在鏈中間換成 RIGHT 會發生什麼。
累積這件事在只有一個 JOIN 的查詢裡不現形,從第二個開始才有分量:某個 LEFT 保護了哪些列,取決於它上面累積出來的是什麼。
左運算元的累積與判讀方法
書店的顧客有三位——佳穎下過兩張單,宗翰與雅文一張都沒有,而兩張單裡只有一張被評價過。要一份完整名單:每位顧客連同他的訂單與那張訂單的評價,沒下單的顧客與沒被評價的訂單也要出現。三張表往下接:
1SELECT 顧客.姓名, 訂單.訂單編號, 評價.星等
2FROM 顧客
3LEFT JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號
4LEFT JOIN 評價 ON 評價.訂單編號 = 訂單.訂單編號;回來四列:
1佳穎 101 5
2佳穎 102 NULL
3宗翰 NULL NULL
4雅文 NULL NULL第二個 LEFT JOIN 的左邊是顧客與訂單併出來的結果,包含宗翰與雅文那兩列補 NULL 的訂單欄。問第二個 LEFT JOIN「左邊是哪一張表」得不到答案,因為左邊已經不是一張表。判讀的作法回到查詢的文字:在要判讀的那個 JOIN 所在的那一行上方畫一條橫線,橫線以上寫到的表合起來是左運算元,那一行自己掛的那張表是右運算元。
由此得到兩個可以直接用的結論。第二個 ON 寫得出顧客表的欄位,因為顧客在左運算元裡面。第二個 JOIN 若換成 INNER,砍掉的是橫線以上那四列裡配不到評價的三列,而不只是「沒有評價的訂單」——宗翰與雅文會一起消失,儘管他們與評價這張表毫無關係:
1SELECT 顧客.姓名, 訂單.訂單編號, 評價.星等
2FROM 顧客
3LEFT JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號 -- 橫線以上:佳穎 101、佳穎 102、宗翰、雅文四列
4JOIN 評價 ON 評價.訂單編號 = 訂單.訂單編號; -- 內連接:四列裡配不到評價的三列一起砍掉
5-- 佳穎 101 5他們那兩列訂單欄裡的 NULL 是連接的產物,不是資料裡本來的空值,而後續對這欄做的計數與比較分不出這件事。LEFT 保護左側、RIGHT 保護右側、FULL 兩側都保護,這三種方向與這些 NULL 的來源定義在 Outer Join(外連接);這些 NULL 怎麼讓聚合算錯、為什麼不能用等號比,由 SQL.6 連接之後的列數與空缺 接手。
鏈中間的 RIGHT JOIN 保護的對象
把最後一個 LEFT 換成 RIGHT,其餘一個字不動:
1FROM 顧客
2LEFT JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號
3RIGHT JOIN 評價 ON 評價.訂單編號 = 訂單.訂單編號結果從四列變成一列,只剩 佳穎 101 5。
RIGHT 保護的是右運算元,而右運算元是評價這一張表。左運算元——顧客與訂單累積出來的那四列——整批降級成可選的,配不上評價的就丟掉。宗翰、雅文、以及佳穎那張沒被評價的訂單,全部消失。
語意上這等於宣告「每一則評價都要留著,而顧客與訂單可有可無」。 書店大概不會想問這個問題。它幾乎不會是任何人在鏈尾接上評價那個 JOIN 時想表達的事,而它讀起來只像是把 LEFT 打成了 RIGHT。
只用 LEFT JOIN 的鏈:保護方向的判讀與 FULL JOIN 的例外
不用 RIGHT 的話,鏈裡每個 JOIN 都是同一個方向:要保護的那張表寫在最上面,往下一行一行掛可選的表。要判斷查詢裡任何一個 JOIN 保護了什麼,在它那一行上方畫橫線就夠了。
真正需要保護的表放到 FROM 之後、寫成鏈最上面那一張就好。
兩張表都需要保護的時候這個做法不適用,那時要的是 FULL JOIN——它兩側都保護,配不上的兩邊各補一次 NULL。鏈裡出現它,「最上面那張表受保護」這個規律就不成立了,判讀要回到逐個連接看保護的方向。要「每一則評價都在、順便看得到它屬於誰」,就從評價開始往下掛訂單與顧客:
1SELECT 顧客.姓名, 訂單.訂單編號, 評價.星等
2FROM 評價
3LEFT JOIN 訂單 ON 訂單.訂單編號 = 評價.訂單編號
4LEFT JOIN 顧客 ON 顧客.顧客編號 = 訂單.顧客編號;
5-- 佳穎 101 5,與鏈尾換成 RIGHT 的那段查詢是同一個列集合這段查詢的每個 JOIN 都是 LEFT,鏈的方向沒有中途反轉。
書寫的左右與引擎的執行順序
左右是書寫位置決定的語意,與引擎實際先處理誰無關。同一個連接寫成 FROM 訂單 JOIN 顧客 與 FROM 顧客 JOIN 訂單,在跑過 ANALYZE 的資料庫上,SQLite 兩次給出完全相同的 query plan——它掃二十萬列的訂單、對五十列的顧客做查找,與誰寫在前面無關。沒有統計時它會改照文字順序走,而那只是它沒有別的資訊時的退路——SQL.1 宣告式的紅利與代價 對照有無統計資訊時引擎怎麼選計畫。
左右決定哪一側的列被保護,執行順序決定誰先被掃,兩者互不相干。 混用這兩個直覺會讓人以為調換書寫順序可以調效能,或以為從查詢文字看得出引擎的動作。
WHERE 引用鏈尾那張表時被撤掉保護的列
WHERE 篩的是整條鏈累積完的那四列。條件引用鏈尾評價表的欄位時,佳穎的 102、宗翰與雅文那三列的評價欄是補出來的 NULL,一起被篩掉——宗翰與雅文跟評價表毫無關係,他們的保護是第一個 LEFT JOIN 給的:
1SELECT 顧客.姓名, 訂單.訂單編號, 評價.星等
2FROM 顧客
3LEFT JOIN 訂單 ON 訂單.顧客編號 = 顧客.顧客編號
4LEFT JOIN 評價 ON 評價.訂單編號 = 訂單.訂單編號
5WHERE 評價.星等 = 5; -- 補出來的 NULL 判成未知,那三列被篩掉
6-- 佳穎 101 5在這份資料上,消失的正好是把第二個 JOIN 換成 INNER 時消失的同一批列。所以判讀一個 WHERE 條件撤掉了誰,同樣回到累積:它引用的表掛在鏈的哪一行,那一行以上累積進來、在那張表上配不到的列都在它的射程裡。條件對 NULL 判成什麼決定撤不撤得掉,哪幾種條件形態保得住保護,在 SQL.5 ON 與 WHERE:連接條件與篩選條件各自的職責。