用子查詢就像在玩脫衣西洋棋:一開始看起來都很簡單,直到你下一步出錯。那錯誤從哪來?通常是因為沒搞懂語法、忽略 SQL 的邏輯特性,或只是沒仔細看。在這堂課我們會聊聊最常見的錯誤,還有怎麼避開它們。
語法錯誤
子查詢對語法很挑剔。漏掉逗號、括號或 alias 都可能讓你的查詢直接掛掉。我們來看幾個常見的問題。
漏掉括號
括號在用子查詢時超級重要。子查詢一定要包在小括號裡,少一對都會語法錯誤。
錯誤範例:
SELECT student_name
FROM students
WHERE student_id IN SELECT student_id FROM enrollments);
錯誤:
ERROR: syntax error at or near "SELECT"
修正:
SELECT student_name
FROM students
WHERE student_id IN (SELECT student_id FROM enrollments);
說明:IN 裡的查詢一定要包在括號裡,這樣 SQL 才知道你要執行子查詢。
沒有 alias
當你在 FROM 裡用子查詢時,記得一定要給它 alias。不然 PostgreSQL 會直接搞混。
錯誤範例:
SELECT student_name, avg_score
FROM (SELECT student_id, AVG(score) AS avg_score FROM grades GROUP BY student_id)
WHERE avg_score > 80;
錯誤:
ERROR: subquery in FROM must have an alias
修正:
SELECT student_name, avg_score
FROM (SELECT student_id, AVG(score) AS avg_score FROM grades GROUP BY student_id) AS subquery
WHERE avg_score > 80;
說明:PostgreSQL 要求每個暫時的表(FROM 裡的子查詢結果)都要有名字。
效能問題
子查詢,尤其沒優化過的,會讓你的資料庫跑得像蝸牛。效能通常是因為重複計算或沒加 index。
重複計算
SELECT 裡的子查詢會對每一行都算一次,超級浪費時間。
範例:
SELECT student_name,
(SELECT COUNT(*) FROM enrollments WHERE enrollments.student_id = students.student_id) AS course_count
FROM students;
如果 students 表有幾萬行,這個子查詢每一行都會重算一次。
優化:
WITH course_counts AS (
SELECT student_id, COUNT(*) AS course_count
FROM enrollments
GROUP BY student_id
)
SELECT s.student_name, c.course_count
FROM students s
LEFT JOIN course_counts c ON s.student_id = c.student_id;
用 CTE(Common Table Expression)或 join 可以避免每行都重算。這個技巧我們之後還會再講 :P
沒加 index
如果你在 WHERE 裡用複雜的子查詢,記得要把需要的欄位加上 index。
範例:
SELECT student_name
FROM students
WHERE student_id IN (SELECT student_id FROM enrollments WHERE course_id = 10);
如果 enrollments 表的 student_id 欄位沒 index,子查詢會全表掃描,超慢。
優化: 建立 index:
CREATE INDEX idx_enrollments_course_id ON enrollments (course_id);
你應該已經常常聽到 index 這個詞了,應該也開始好奇它到底是什麼。不過再等等,之後會詳細講。Index 很重要,但它主要是讓查詢更快,不是讓爛查詢變好查詢。它們會讓你在 production 上查詢快很多,尤其是資料表有幾百萬行的時候。
邏輯錯誤
子查詢的邏輯錯誤跟語法錯誤一樣常見。問題通常是沒搞懂 NULL、過濾條件或聚合函數的用法。
NULL 用錯
NULL 是新手的陷阱。在子查詢裡用 IN 或 NOT IN,如果有 NULL,結果會被影響。
錯誤範例:
SELECT student_name
FROM students
WHERE student_id NOT IN (SELECT student_id FROM enrollments);
如果 enrollments 表裡有 student_id = NULL 的行,這個查詢會回傳空結果。因為 NOT IN 條件會變成 NULL IS NOT IN。
修正:
SELECT student_name
FROM students
WHERE student_id NOT IN (SELECT student_id FROM enrollments WHERE student_id IS NOT NULL);
只要用 NOT IN,就一定要過濾掉 NULL。
過濾條件寫錯
子查詢常常用來做複雜過濾,但條件寫錯,結果就完全不是你想要的。
錯誤範例:
SELECT student_name
FROM students
WHERE (SELECT AVG(score) FROM grades WHERE grades.student_id = students.student_id) > 80;
如果有學生沒有成績,子查詢會回傳 NULL,這個學生就不會出現在結果裡。
修正:
SELECT student_name
FROM students
WHERE COALESCE((SELECT AVG(score) FROM grades WHERE grades.student_id = students.student_id), 0) > 80;
用 COALESCE 把 NULL 換成預設值。
避免錯誤的建議
想要避免用子查詢時踩雷,記得這幾點:
括號和 alias 要用對。 有問題時先檢查括號有沒有關好,子查詢有沒有 alias。
查詢要優化。 能用 JOIN、WITH 或 index 的地方就不要硬用子查詢。
注意 NULL。 子查詢裡可能會有 NULL,記得用 IS NOT NULL、COALESCE 或類似的寫法。
多測試。 每個子查詢都要單獨測試,確定它回傳你要的結果。
可讀性。 用縮排和 alias 讓你的 code 好讀。記住:一個月後你自己也可能看不懂你寫了什麼。
GO TO FULL VERSION