Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
撰寫的順序,不是執行的順序
令人困惑的 SQL 錯誤,多半源自同一件事:您撰寫子句的順序,並不是資料庫評估它們的順序。 SELECT 寫在最前面,但引擎處理到它的時間點很晚。所以這段會失敗:
SELECT price * quantity AS total
FROM order_items
WHERE total > 1000; -- 錯誤:找不到 total 這個欄位
而這段可以:
SELECT price * quantity AS total
FROM order_items
ORDER BY total DESC; -- 正常
別名本身完全沒變,變的是該子句是在 SELECT 之前還是之後執行。
兩種順序並列
| 撰寫順序 | 邏輯執行順序 |
|---|---|
1. SELECT | 1. FROM / JOIN — 組出資料列 |
2. FROM | 2. WHERE — 逐列過濾 |
3. JOIN | 3. GROUP BY — 把資料列收攏成群組 |
4. WHERE | 4. HAVING — 過濾群組 |
5. GROUP BY | 5. SELECT — 計算輸出欄位(別名在這裡誕生) |
6. HAVING | 6. DISTINCT — 去除重複列 |
7. ORDER BY | 7. ORDER BY — 排序結果 |
8. LIMIT | 8. LIMIT / OFFSET — 裁切結果 |
把右欄由上往下讀一遍,SQL 的多數「為什麼」就不再是謎。這是標準定義的邏輯順序;只要結果一致,最佳化器可以自由調整實際的執行方式。
為什麼 WHERE 不能用別名,ORDER BY 可以
SELECT 是第 5 步,WHERE 是第 2 步。WHERE 執行時別名還不存在;ORDER BY 是第 7 步、在 SELECT 之後,那時就存在了。
可攜性最好的做法是把運算式再寫一次:
SELECT price * quantity AS total
FROM order_items
WHERE price * quantity > 1000;
或是塞進子查詢,讓別名對外層查詢而言變成真正的欄位:
SELECT * FROM (
SELECT price * quantity AS total FROM order_items
) t
WHERE t.total > 1000;
各方言放寬這條規則的程度不同:MySQL 在 GROUP BY 與 HAVING 允許別名,PostgreSQL 在 GROUP BY 與 ORDER BY 允許但 HAVING 不行。不過主流資料庫都不允許在 WHERE 使用別名。
WHERE 與 HAVING 的分工
兩者不能互換,原因同樣是順序:WHERE(第 2 步)過濾的是分組之前的資料列,HAVING(第 4 步)過濾的是彙總之後的群組。
SELECT customer_id, COUNT(*) AS order_count
FROM orders
WHERE status = 'paid' -- 先過濾資料列:只留已付款的訂單
GROUP BY customer_id
HAVING COUNT(*) >= 3; -- 再過濾群組:這樣的訂單有 3 筆以上
對調之後意義就完全變了。HAVING status = 'paid' 是在問群組而不是資料列,而 WHERE COUNT(*) >= 3 則直接是錯誤——第 2 步時還沒開始計算任何彙總。
實務判準很單純:條件裡出現彙總函式就放 HAVING,否則放 WHERE。 把非彙總條件留在 WHERE,也能讓引擎更早丟掉資料列,通常比較快。
GROUP BY:各家資料庫不同的規則
分組時,SELECT 列出的每個欄位都必須出現在 GROUP BY,或是被彙總函式包起來。否則資料庫無從得知該顯示收攏後的哪一列。
-- 壞掉的寫法:不知道該用哪個 name 搭配計數
SELECT customer_id, name, COUNT(*) FROM orders GROUP BY customer_id;
PostgreSQL 會直接拒絕。MySQL 預設也拒絕——自 5.7.5 起 ONLY_FULL_GROUP_BY 模式預設開啟。舊設定的 MySQL 會接受並回傳任意一列的值,因此常以升級伺服器後既有查詢突然失敗的形式浮現(expression is not in GROUP BY clause and contains nonaggregated column)。正解是把欄位加進 GROUP BY 或改用 MAX(name) 之類的彙總,而不是關掉該模式。
彙總與 NULL
以下兩點會悄悄改變數字,值得記住:
| 運算式 | 會算進 NULL 嗎 |
|---|---|
COUNT(*) | 會(計算列數本身) |
COUNT(欄位) | 不會(該欄位為 NULL 的列會跳過) |
COUNT(DISTINCT 欄位) | 不會(非 NULL 的相異值個數) |
SUM / AVG / MAX / MIN | 不會(忽略 NULL) |
AVG(欄位) 是除以非 NULL 值的個數,不是列數。若希望 NULL 視為 0,請明確寫成 AVG(COALESCE(欄位, 0))。
影響最大的場合是 LEFT JOIN 之後:沒有配對到的列右側會是 NULL,因此 COUNT(*) 會把沒有訂單的客戶算成 1,而 COUNT(o.id) 才會正確地算成 0。
SELECT c.id, COUNT(o.id) AS order_count -- 不是 COUNT(*)
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id;
LIMIT 最後才跑,而且怕同分
LIMIT 是第 8 步,在排序之後才套用。由此可得兩個結論。
它不會讓彙總變便宜。 對含 GROUP BY 的查詢加上 LIMIT 10,資料庫仍會先分組完再把大部分丟掉。
沒有唯一排序鍵,分頁就不穩定。 若 created_at 有同分,翻頁過程中同一列可能出現兩次,或一次都沒出現。請加上唯一欄位當作決勝條件:
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 40;
語法有方言差異:PostgreSQL 與 MySQL 都接受 LIMIT n OFFSET m(MySQL 另保留參數順序相反的舊寫法 LIMIT m, n),SQL Server 則是 OFFSET m ROWS FETCH NEXT n ROWS ONLY。
JOIN 與 WHERE 的陷阱
這是順序造成的事故中最常見的一種。JOIN 是第 1 步、WHERE 是第 2 步——也就是說,針對右側資料表的 WHERE 條件會在外部連接填入 NULL 之後才執行,而那些 NULL 列無法通過條件,於是消失:
-- 實際上變成了 INNER JOIN
SELECT * FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'paid';
-- 沒有已付款訂單的客戶也會保留
SELECT * FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'paid';
過濾連接的條件寫在 ON,過濾最終結果的條件寫在 WHERE。 各種 JOIN 造成的結果列數差異,整理在 SQL JOIN 完整參考。
不必背順序也能組裝查詢
知道順序是一回事,每次都把子句寫在正確位置又是另一回事。Visual SQL Builder 讓您從表單挑選資料表、連接條件、過濾、分組與排序,並依正確的撰寫順序輸出子句。由於每加一個元件就能看到產生的 SQL 改變,也很適合用來確認「這個條件到底該放哪裡」。
要讀懂既有查詢,可以用 SQL 格式化工具 縮排,子句的邊界就會變得清楚;SQL 轉 ER 圖 則能把正在連接的資料表關係畫成圖。若還在設計資料表階段,CREATE TABLE 參考手冊 整理了型別與約束。這些工具都在瀏覽器內完成處理,貼上的查詢與結構不會傳送到外部。
總結
- 執行順序為
FROM→WHERE→GROUP BY→HAVING→SELECT→DISTINCT→ORDER BY→LIMIT - 別名誕生於
SELECT,所以WHERE看不到、ORDER BY看得到 - 不含彙總的條件放
WHERE,含彙總的條件放HAVING - MySQL 自 5.7.5 起
ONLY_FULL_GROUP_BY為預設值 COUNT(*)算列數,COUNT(欄位)會跳過 NULL;LEFT JOIN之後幾乎都該用後者LIMIT最後才跑,穩定分頁需要在ORDER BY加上唯一的決勝欄位- 過濾連接用
ON,過濾結果用WHERE
常見問題
為什麼別名在 ORDER BY 可以用,在 WHERE 卻不行?
因為 WHERE 在 SELECT 之前評估,ORDER BY 在之後。別名是由 SELECT 建立的,所以 WHERE 執行時它根本還不存在。請在 WHERE 裡重寫完整的運算式,或用子查詢包起來讓別名成為真正的欄位。
WHERE 和 HAVING 該怎麼分?
WHERE 過濾分組前的個別資料列,HAVING 過濾彙總後的群組。條件若用到 COUNT()、SUM() 等彙總函式,就只能放在 HAVING;其餘應留在 WHERE,因為那樣可以更早排除資料列。
MySQL 出現「expression is not in GROUP BY clause」怎麼辦?
這是自 MySQL 5.7.5 起預設開啟的 ONLY_FULL_GROUP_BY 模式。SELECT 中的每個欄位都必須出現在 GROUP BY 或被彙總函式包住。針對舊版 MySQL 撰寫的查詢在升級後失敗多半是這個原因,正解是把欄位加進 GROUP BY 或加以彙總,而不是關閉該模式。
加上 LIMIT 會讓彙總查詢變快嗎?
一般不會。LIMIT 是在分組與排序全部完成之後才套用,資料庫仍然會先建立完整結果再丟棄。想讓它變快,請用 WHERE 提早縮小範圍,或考慮能協助分組的索引。
貼上的 SQL 會傳送到伺服器嗎?
不會。Visual SQL Builder 與 SQL 格式化工具 都在瀏覽器內完成處理,輸入的查詢與資料表名稱不會傳送到任何地方。