求值順序是語意模型規定的先後:FROM 決定要處理哪個資料集合、WHERE 篩掉列、GROUP BY 分組、HAVING 篩掉組、SELECT 算出要輸出的欄位、ORDER BY 排序、LIMIT 取前面幾列。它與書寫順序不同,寫在最前面的 SELECT 在這裡排在倒數第二。

求值順序規定的是每一步能用到哪些欄位與值——而不在這一步能用的欄位與值裡的,那一步就用不了,引擎因此會擋下某些寫法。擋下來的理由分兩種,分法看換一家引擎結果會不會變:這一步確實拿不到那個值的,本篇叫模型層的限制,換哪一家引擎都一樣;照模型拿不到、而有的引擎替使用者補上一個決定的,叫引擎寬鬆,換一家就變。在擋下它的那一家引擎上,模型層的限制與引擎寬鬆的錯誤訊息長得一樣。引擎實際照什麼次序做事是另一種順序(執行順序),由最佳化器決定而不在本篇範圍,SQL.1 宣告式的紅利與代價 把書寫、求值、執行三種順序分開。

求值順序各步驟可用的欄位與值

FROM 走完之後,這一步的中間結果是一個 relation,欄位是來源表的欄位。WHERE 因此只能引用這些欄位,一列一列判斷留不留。本篇把 FROM 當成一個交出關係的整體;多張表的時候它自己是一個過程,按書寫位置逐個把表併進來,SQL.4 鏈式連接的左運算元 寫這個累積怎麼決定每個 ON 引用得到哪些欄位。

GROUP BY 之後中間結果的單位從列換成組,這是整個順序裡變化最大的一步。原本每一列各自獨立,之後每一組是一個單位,而組裡那些列的個別值不再直接可用。

HAVING 篩掉的是組,所以它能用聚合函數——每一組的計數、總和、最大值到這一步都算得出來了。SELECT 到這裡才把要輸出的欄位算出來,別名也是在這一步才誕生。ORDER BY 排的是已經算好的輸出,所以它用得到別名。LIMIT 在最後,它切的是排序完的結果——沒有 ORDER BY 的 LIMIT 取到哪一列沒有保證,因為前面沒有任何一步決定過順序。

模型層的限制:把聚合放進 WHERE

書店有三位顧客,訂單表裡兩張都掛在佳穎名下。要找出下過一張以上訂單的顧客,把「訂單張數大於一」這個條件寫進 WHERE:

1SELECT 顧客編號 FROM 訂單 WHERE COUNT(*) > 1 GROUP BY 顧客編號;

SQLite 回 misuse of aggregate: COUNT(),DuckDB 回 WHERE clause cannot contain aggregates。同一個問題移到 HAVING:

1SELECT 顧客編號, COUNT(*) AS 張數 FROM 訂單 GROUP BY 顧客編號 HAVING COUNT(*) > 1;
2-- 兩個引擎都回 (1, 2):顧客編號 1 的佳穎,兩張訂單

差別在分組發生了沒有。WHERE 那一步處理的還是一列一列的訂單,「這一組有幾張」這個值在那個時刻還不存在,所以這是模型層的限制而不是功能缺口。這個限制與引擎的寬鬆度(引擎收下不合標準的寫法、自己補上一個決定)無關;寬鬆度在各家怎麼分、差異在什麼位置發聲,在 SQL.19 引擎寬鬆度與可攜性:各家對同一組寫法的差異、分級與處理時機。

引擎寬鬆:WHERE 用 SELECT 的別名

按求值順序,WHERE 早於 SELECT,別名那時候還沒誕生。標準照這個推論禁止,PostgreSQL 也照這個推論拒絕。

而 SQLite 與 DuckDB 都接受。書店查佳穎這一列,把姓名欄改名之後在 WHERE 用新名字:

1SELECT 姓名 AS 客戶 FROM 顧客 WHERE 客戶 = '佳穎';
2-- SQLite 與 DuckDB 都回 ('佳穎',);PostgreSQL 報 column "客戶" does not exist

這個放寬的範圍不是照求值順序推出來的。 GROUP BY 與 HAVING 同樣早於 SELECT,而 SQLite 與 DuckDB 在這兩個位置接受別名:

1SELECT substr(下單日,1,7) AS 月份, count(*) AS 張數
2FROM 訂單 GROUP BY 月份;   -- GROUP BY 引用的是 SELECT 那一步才取的別名
3-- SQLite:2026-03 | 2

所以「這一步還沒算到別名」只推得出候選,推不出答案:各家對哪幾個位置放寬、放寬到什麼程度,要各自查。

而放寬是疊在語意模型上面的,沒有取代它。可以從一個細節看出疊的位置:來源表本來就有同名欄位的時候,WHERE 綁的是來源欄位而不是別名。實測用一張只有 x 與 y 兩欄的表,把 y 取名成 x 之後在 WHERE 寫 x = 1:

1CREATE TABLE t (x INT, y INT);
2INSERT INTO t VALUES (1, 2), (2, 1);
3
4SELECT y AS x FROM t WHERE x = 1;
5-- 2

WHERE 綁的若是別名,比對的是 y = 1,會回第二列的 1;實際回的是 2,代表 SQLite 拿來源欄位 x 去比對,DuckDB 也是這樣。來源表沒有同名欄位的時候,引擎才會讓別名接手這個名字。

列的條件放進 HAVING:DuckDB、MySQL 與 SQLite 各自的反應

把聚合放進 WHERE 這種放錯邊,每一家都會擋下來。反過來把列的條件放進 HAVING,後果分兩家,而其中一家不報錯。

這一節要多幾則評價才看得出來,所以換掉共用資料裡那一則,並多加一張訂單 103——換完之後訂單 101 有五星與三星各一則,訂單 102 有兩則五星,訂單 103 有一則三星:

1INSERT INTO 訂單 VALUES (103, 2, '2026-03-20', 400);
2DELETE FROM 評價;
3INSERT INTO 評價 VALUES (1,101,5),(2,101,3),(3,102,5),(4,102,5),(5,103,3);

要問的是「哪些訂單有兩則以上的五星評價」。正確的寫法把星等這個列的條件放 WHERE:

1SELECT 訂單編號, count(*) FROM 評價
2WHERE 星等 = 5 GROUP BY 訂單編號 HAVING count(*) >= 2;
3-- 102 | 2

只有 102,因為 101 的兩則評價裡只有一則是五星。走一遍就看得到為什麼:WHERE 先砍掉三星那兩列,剩下三列拿去分組,組 101 只剩一列而組 102 有兩列,HAVING 再篩掉組 101。

把星等的條件搬進 HAVING:

1SELECT 訂單編號, count(*) FROM 評價
2GROUP BY 訂單編號 HAVING 星等 = 5 AND count(*) >= 2;

DuckDB 拒絕,訊息是 column 星等 must appear in the GROUP BY clause——分組之後組裡有兩個不同的星等,「這一組的星等」沒有定義。MySQL 8 也拒絕,而它擋在更前面的一步:

1ERROR 1054 (42S22): Unknown column '星等' in 'having clause'

MySQL 的 HAVING 只認得分組鍵、聚合,以及 SELECT 裡取好的別名,而 星等 三樣都不是,所以它在解析名字那一步就停了。這一種擋法與設定無關——把 sql_mode 清空再跑,訊息一個字都沒變。

管寬鬆度的那個設定叫 ONLY_FULL_GROUP_BY,從 MySQL 5.7 起是預設值,而它管的是另一個形態:把非分組欄放進 SELECT 的寫法(SELECT 訂單編號, 星等 FROM 評價 GROUP BY 訂單編號)開著時回 ERROR 1055 並在訊息裡點名這個設定,關掉之後就與 SQLite 一樣從組裡任意挑一列。

SQLite 接受,回 101 與 102。 兩張訂單都被報成「有兩則以上五星」,而 101 只有一則。SQLite 從組裡取了其中一列的星等拿去比對,而它取到的剛好是 101 那組裡五星的那一列,於是整組通過。

至於它會取到哪一列,多數時候規格不保證。唯一的例外是同一個 SELECT 裡只有一個 min() 或 max() 的時候——那時 SQLite 保證非分組欄取自那個極值所在的列,這一點有明文規定。除此之外不要依賴它挑中哪一列。

SQLite 收下並給出錯的清單,比 DuckDB 與 MySQL 當場擋下危害更高,因為輸出看起來完全合理。 每組一列、欄位齊全、訂單編號都是實際存在的訂單——沒有任何一處會讓人停下來檢查。放錯邊的那個條件在報錯的引擎上寫的當下就會發現,在寬鬆的引擎上會一路帶著錯的清單走下去。

同一個機制還有一個比較溫和的形態:SELECT 顧客編號, 下單日 FROM 訂單 GROUP BY 顧客編號,DuckDB 同樣拒絕、SQLite 回一個從組裡任意挑的日期。非分組欄放進 SELECT 只讓輸出的值不確定,列的條件放進 HAVING 讓整份清單錯掉。

模型層的限制與引擎寬鬆:分辨的依據與換引擎時的表現

分開模型層的限制與引擎寬鬆的,是引擎手上有沒有一個值可以補上。分組還沒發生的時候,「這一組有幾張」這個值本身還沒有被算出來,引擎沒有值可補,所以聚合放進 WHERE 是模型層的限制。SELECT 那一步還沒算的時候,別名這個名字還沒誕生,而它要指的值早就算好了——差的只是一個稱呼,有些引擎因此願意替使用者把名字對回去。分組之後「這一組的星等」在模型裡沒有定義,而組裡每一列都有自己的星等,SQLite 從中挑一列的值補上。別名放進 WHERE 與列的條件放進 HAVING 都是引擎寬鬆:模型沒給的決定,由引擎替使用者補上。

模型層的限制與引擎寬鬆在換引擎時的表現不同。碰到模型層的限制的寫法,第一次執行就會被擋下來;靠引擎寬鬆跑起來的寫法會安靜跑過,等到換引擎才出現差異——而其中最麻煩的是從組裡任意挑一列的寬鬆:在兩家都寬鬆的引擎之間搬家(SQLite,以及關掉 ONLY_FULL_GROUP_BY 的 MySQL),連報錯都沒有,只是挑到的那一列可能不同。

求值順序在連接上的實例:條件放 ON 還是 WHERE

HAVING 那一節已經露出一個問題:同一個條件擺在哪一步,決定的是留下哪些列,而寬鬆的引擎不會替擺錯的那一種報錯。這個問題在連接上重演一次。ON 是 FROM 那一步內部的條件,WHERE 是下一步,所以條件放 ON 還是 WHERE,是在求值順序的相鄰兩步之間選一步——兩種擺法都合法、都不報錯,而結果差在沒配到的那些列留不留下來。SQL.5 ON 與 WHERE:連接條件與篩選條件各自的職責 用同一組書店資料把兩種擺法的結果並排,並給一個代進 NULL 就看得出差別的測試。兩種擺法的差別由語意模型規定,SQLite、PostgreSQL 與 MySQL 對同一種擺法回同一批列,所以條件擺錯位置的時候,換一家引擎也不會浮現。在 SQL.13 查詢的合法性與答案的正確性:引擎檢查的範圍、答案錯掉的成因與查證方法 按斷在哪一個環節分的成因裡,它屬於誤解語意模型:寫的人預期 WHERE 裡的條件只影響配對,而模型規定它篩的是配對之後的結果。換一家引擎就浮現的是引擎寬鬆,HAVING 那一節 SQLite 收下的列條件是一例。