你寫的 SQL 不是照你寫的順序跑
· tech
你寫 SQL,幾乎都是 SELECT 開頭。寫久了很自然會以為:它就是從 SELECT 開始跑的。但不是——SQL 是宣告式的,你寫的是「要什麼」,引擎自己決定「怎麼跑、照什麼順序跑」,而它跑的順序跟你寫的順序差很多。這篇是整個系列的脊椎:把這個順序刻進腦子,後面一票「為什麼這樣寫會錯」的謎題,會一次全解開。
你這樣寫,它不這樣跑
你習慣的書寫順序是 SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT。但引擎真正的邏輯執行順序是這樣:
SELECT 開頭,但它幾乎最後才跑(第 5 位)。真正的順序是 FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY→LIMIT——記住這條線,下面一票「為什麼會錯」就自己解開了一句話記住:先決定「從哪來、留哪些列」,再「分組、篩組」,然後才「算出你要的欄位」,最後排序、取數。 SELECT 只是你寫在最前面,它其實排在第五。
一次解開三個「為什麼」
這個順序不是冷知識,它直接解釋了三個幾乎每個人都踩過的坑。
為什麼 WHERE 不能用 SELECT 取的別名? 因為 WHERE(②)比 SELECT(⑤)早跑,別名那時候還不存在:
SELECT price * qty AS revenue
FROM orders
WHERE revenue > 1000; -- ❌ revenue 此時還沒被算出來
SELECT price * qty AS revenue
FROM orders
WHERE price * qty > 1000; -- ✅ 直接寫算式(或包一層子查詢/CTE)
為什麼同一個別名,ORDER BY 卻可以用? 因為 ORDER BY(⑥)比 SELECT(⑤)晚跑,這時 revenue 已經誕生了:
SELECT price * qty AS revenue
FROM orders
ORDER BY revenue DESC; -- ✅ 排序時別名已存在
為什麼 window function 不能塞進 WHERE? 同一個道理——window function 在 SELECT(⑤)階段才計算,WHERE(②)時它根本還沒發生:
-- ❌ 想「只留每類排名第一」,但 WHERE 時 rn 還沒算出來
SELECT *, ROW_NUMBER() OVER (PARTITION BY cat ORDER BY sales DESC) AS rn
FROM products
WHERE rn = 1;
-- ✅ 標準解法:包成子查詢,在外層才篩
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY cat ORDER BY sales DESC) AS rn
FROM products
) t
WHERE rn = 1;
那個「為什麼 window 一定要包一層子查詢」的千古疑問,答案就是這張圖:它算得比 WHERE 晚。(window function 是系列第 5 篇的主角,這裡先知道它的位置。)
WHERE 篩「列」,HAVING 篩「組」
順序也一次講清楚 WHERE 和 HAVING 的差別:一個在分組前、一個在分組後,所以它們過濾的東西根本不同:
WHERE 砍的是「列」、在分組前;HAVING 砍的是「組」、在分組後。位置不同,能做的事就不同放進一個例子就很清楚:
SELECT customer_id, SUM(amount) AS total
FROM orders
WHERE amount > 0 -- ② 先逐列剔除無效資料(退款、0 元)
GROUP BY customer_id
HAVING SUM(amount) > 1000; -- ④ 分組後,只留總額破千的『組』
這裡還藏了一個效能原則:能用 WHERE 先砍掉的列,別留到 HAVING。 WHERE 在分組前就把資料變小,後面要分組、要算的東西都少了;丟給 HAVING 等於讓一堆注定被淘汰的列先跑完分組,白做工。這個「越早過濾越好」的直覺,跟我在 Spark 那篇講的「先 filter 把資料變小再 shuffle」是同一件事。
反思
記住「跑的順序」,一半的 SQL 謎題自己會解
我剛開始寫 SQL 時,把「別名不能在 WHERE 用」「window 要包子查詢」「WHERE 跟 HAVING 到底差在哪」當成三條各自要背的規則。後來才發現它們是同一件事的三個切面——你寫的順序不是它跑的順序。一旦把那張執行順序圖刻進腦子,這些就不再是要死背的規則,而是「當然會這樣」的推論。我現在遇到別人問「這個 query 為什麼報錯」,第一個反射動作就是在腦中跑一遍 FROM→WHERE→GROUP BY→…,八成當場就看出是哪個階段引用了還沒誕生的東西。把規則還原成機制,是我學任何東西的第一步,SQL 也不例外。
這其實又是「宣告式」的一體兩面
SQL「你寫的順序 ≠ 執行的順序」,本質上跟我在 K8s、Spark DataFrame 反覆講的宣告式是同一種東西:你描述「要什麼」,引擎決定「怎麼做、照什麼順序做」。好處是你不用管執行細節、引擎還能幫你最佳化;代價是——你不能假設它照你寫的字面順序跑。 這就是為什麼「懂執行模型」對 SQL 這麼關鍵:不懂,你會寫出一堆「看起來對、其實錯」或「跑得出來、但慢得莫名其妙」的 query;懂了,你才有能力預測、也才有能力調。宣告式工具都是這樣,方便的背面,是你得花力氣去理解那台替你做決定的引擎。
「先把資料變小」是跨工具的共同直覺
WHERE 早於 HAVING、能砍的列盡早砍——這個在 SQL 裡的小習慣,放大到任何資料系統都成立。Spark 要你先 filter 再 shuffle、把 filter 下推到掃描層;這裡是先 WHERE 再 GROUP BY。背後是同一句話:越晚處理的資料,每一列都更貴(它得先扛過前面所有階段)。所以我看任何一段資料處理,不管是 SQL、Spark 還是一條 pipeline,都會先問同一題:最會過濾掉資料的那一步,能不能再往前挪? 這是投報率最穩定的一種最佳化——不靠聰明,只靠順序對。