有時候我們不只是要把資料 group 起來然後過濾結果,還想加點額外邏輯——比如說,把學生在 group 裡的平均分數跟某個外部標準比一比。這時候 HAVING 加子查詢就超好用,讓你可以直接在 SQL 查詢裡做更聰明的判斷。
回顧一下 HAVING
我們來專注看看怎麼把子查詢跟 HAVING 結合,用來在聚合值的層級過濾資料。為什麼要這樣?因為 WHERE 只能過濾單一 row,但 HAVING 是針對 group 過的資料——這是另一個分析層級,讓你有更多彈性。
在我們 dive in 子查詢加 HAVING 之前,先 refresh 一下 HAVING 是什麼,跟 WHERE 有什麼不一樣。
WHERE會在 group 之前 過濾 row(GROUP BY之前)。HAVING則是在 聚合之後,也就是 group 完才過濾。
想像你在分析學生跟他們的分數。用 WHERE 可以先把分數太低的學生排除掉,但 HAVING 則可以直接根據 group 的平均或最大分數,把整個 group 排除。
資料範例
這裡有一張學生的範例表:
資料表 students:
| student_id | student_name | department | grade |
|---|---|---|---|
| 1 | Alex | Physics | 80 |
| 2 | Maria | Physics | 85 |
| 3 | Dan | Math | 90 |
| 4 | Lisa | Math | 60 |
| 5 | John | History | 70 |
用 HAVING 的範例(沒用子查詢)
SELECT department, AVG(grade) AS avg_grade
FROM students
GROUP BY department
HAVING AVG(grade) > 75;
結果:
| department | avg_grade |
|---|---|
| Physics | 82.5 |
| Math | 75.0 |
History 系沒被選進來,因為他們的平均分數不到 75。很簡單對吧?現在我們來加點子查詢的魔法。下一個例子,我們可以根據所有系的總平均來過濾。
HAVING 裡的子查詢
HAVING 裡的子查詢超級靈活,讓你可以在過濾聚合資料時加上更多條件。你可以把平均分、最大值這些聚合結果,跟資料庫其他地方算出來的值比一比。簡單說,就是可以問:「我們這個 group 的表現有沒有比全校平均好?」
範例:用平均分過濾系所
假設我們想找出那些學生表現比其他系都好的系——也就是平均分比全校平均還高的系。
這是我們的資料:
資料表 students:
| student_id | student_name | department | grade |
|---|---|---|---|
| 1 | Alex | Physics | 80 |
| 2 | Maria | Physics | 85 |
| 3 | Dan | Math | 90 |
| 4 | Lisa | Math | 60 |
| 5 | John | History | 70 |
先算出全校學生的平均分:
SELECT AVG(grade) AS university_avg
FROM students;
現在我們在 HAVING 裡用子查詢:
SELECT department, AVG(grade) AS avg_grade
FROM students
GROUP BY department
HAVING AVG(grade) > (SELECT AVG(grade) FROM students);
結果:
| department | avg_grade |
|---|---|
| Physics | 82.5 |
這裡發生了什麼?
- 子查詢(
SELECT AVG(grade) FROM students)會算出全校平均分——這裡是 77。 - 主查詢把學生依系 group 起來,算每個系的平均分。
HAVING拿系的平均分去跟全校平均比,只留下比全校高的系。
用 WHERE 跟 HAVING 的比較
為了更清楚差別,假設你想選出那些分數高於平均的學生。這只能用 WHERE:
SELECT name, grade
FROM students
WHERE grade > (SELECT AVG(grade) FROM students);
結果(用前面那張表):
| name | grade |
|---|---|
| Alex | 80 |
| Maria | 85 |
| Dan | 90 |
但如果你想看哪些系的平均分高於全校平均,就一定要用 HAVING——因為你不是在過濾 row,而是在過濾 group:
SELECT department, AVG(grade) AS avg_grade
FROM students
GROUP BY department
HAVING AVG(grade) > (SELECT AVG(grade) FROM students);
結果:
| department | avg_grade |
|---|---|
| Physics | 82.5 |
重點:
WHERE是在 group 之前處理單一 row。HAVING是 group 完之後才過濾 group。
範例:同時處理多個聚合
我們再來看一個例子。假設你有一張 students 表,裡面有學生分數跟系所:
資料表 students:
| name | grade | department |
|---|---|---|
| Alex | 80 | Physics |
| Maria | 85 | Physics |
| Dan | 90 | Math |
| Olga | 95 | Math |
| Ivan | 70 | History |
| Nina | 75 | History |
現在我們想找出那些:
- 學生平均分 高於 全校平均。
- 該系最高分 大於 90。
查詢如下:
SELECT department, AVG(grade) AS avg_grade, MAX(grade) AS max_grade
FROM students
GROUP BY department
HAVING AVG(grade) > ( SELECT AVG(grade) FROM students )
AND MAX(grade) > 90;
這個查詢在做什麼:
AVG(grade)> (SELECT AVG(grade) FROM students) —— 檢查這個系的平均分有沒有比全校高。MAX(grade)> 90 —— 這個系有沒有人考超過 90。
結果:
| department | avg_grade | max_grade |
|---|---|---|
| Math | 92.5 | 95 |
只有 Math 系同時平均分高於全校,還有一個人考超過 90。
範例:找出分數差最小的 group
假設你想找出那些 group,裡面最高分跟最低分的差,比全校的差還小。
這是我們要用的 students 表:
| name | grade | department |
|---|---|---|
| Alex | 80 | Physics |
| Maria | 85 | Physics |
| Dan | 90 | Math |
| Olga | 95 | Math |
| Ivan | 70 | History |
| Nina | 75 | History |
我們把問題拆成幾步:
- 先算全校最高分減最低分:
SELECT MAX(grade) - MIN(grade) AS range_university FROM students; - 然後寫主查詢,跟這個子查詢比:
SELECT department, MAX(grade) - MIN(grade) AS range_department
FROM students
GROUP BY department
HAVING (MAX(grade) - MIN(grade)) < ( SELECT MAX(grade) - MIN(grade) FROM students );
查詢結果:
| department | range_department |
|---|---|
| Physics | 5 |
| Math | 5 |
Physics 跟 Math 這兩個 group 的分數分布比較穩定——他們的分數差比全校還小。
優化 HAVING 加子查詢的查詢
要記得,巢狀子查詢在大資料庫裡會很吃效能。這裡有幾個小建議:
用 index。 如果子查詢是針對 WHERE 或 JOIN 的欄位,記得加 index。
避免資料爆炸。 如果子查詢回傳太多中間結果,可以拆成幾步或用暫存表。
用 EXPLAIN 來 profile 查詢。 隨時檢查 PostgreSQL 怎麼執行你的查詢。如果發現子查詢被執行很多次,想想怎麼優化。
考慮用 CTE。 有時候 WITH(Common Table Expressions)會更快也更好讀。這個我們之後會再講 :P
子查詢、HAVING 跟 GROUP BY 的組合
用 HAVING 裡的子查詢可以寫出更複雜的過濾條件,特別是你要同時考慮聚合、平均值或其他指標時。這些技巧都能幫你在真實資料裡找到有趣的 insight。
範例:同時比較系的平均分跟學生數
假設你想選出那些:
- 平均分高於全校平均的系。
- 學生數比平均分最低的系還多。
這是原始 students 表:
| name | grade | department |
|---|---|---|
| Alex | 80 | Physics |
| Maria | 85 | Physics |
| Dan | 90 | Math |
| Olga | 95 | Math |
| Ivan | 70 | History |
| Nina | 75 | History |
| Oleg | 60 | History |
查詢:
SELECT department, AVG(grade) AS avg_grade, COUNT(*) AS student_count
FROM students
GROUP BY department
HAVING AVG(grade) > ( SELECT AVG(grade) FROM students )
AND COUNT(*) > (
SELECT COUNT(*)
FROM students
GROUP BY department
ORDER BY AVG(grade)
LIMIT 1
);
這個查詢展示了怎麼把 HAVING 裡的子查詢跟 GROUP BY 結合,讓你一次分析多個條件。結果:
| department | avg_grade | student_count |
|---|---|---|
| Physics | 82.5 | 2 |
| Math | 92.5 | 2 |
History 系沒被選進來,因為他們平均分最低、學生數也最少。Physics 跟 Math 這兩個系都同時在分數跟人數上超過平均。
常見錯誤跟怎麼避免
遇到 NULL 的問題。 如果資料有 NULL,用 HAVING 加子查詢可能會出現怪結果。記得用 COALESCE 處理:
SELECT AVG(grade)
FROM students
WHERE grade IS NOT NULL;
子查詢回傳太多資料。 如果子查詢結果太多,效能會很差。記得把條件寫清楚。
搞錯執行順序。 要記得 HAVING 是在 group 之後才執行,子查詢有時候會在主查詢之前跑。
沒加 index。 如果子查詢用到的欄位沒 index,查詢會變超慢。
HAVING 裡的子查詢讓你可以在聚合層級做超多分析。你可以根據複雜條件過濾 group,比較 group 之間的結果,寫出很強的分析查詢。恭喜你,現在已經 ready 把這些技巧用在實戰專案啦!
GO TO FULL VERSION