恭喜啦,我們終於來到真正有趣的地方!今天要來看看怎麼把不同類型的子查詢混在一起用,解決一些比較 tricky 的問題。EXISTS、IN、HAVING 這三個組合起來,你就像資料庫魔法師一樣。等等會從一張表撈資料、根據另一張表的資料過濾、分組,然後再過濾分組結果。Bonus:還會聊聊怎麼讓查詢更有效率的小技巧。
我們先來設定一個總體目標,這堂課會一步步把它解出來。
題目設定
假設我們有一個大學的資料庫,裡面有三張表:
表格 students
| id | name | group_id |
|---|---|---|
| 1 | Otto | 101 |
| 2 | Maria | 101 |
| 3 | Alex | 102 |
| 4 | Anna | 103 |
表格 courses
| id | name |
|---|---|
| 1 | 數學 |
| 2 | 程式設計 |
| 3 | 哲學 |
表格 enrollments
| student_id | course_id | grade |
|---|---|---|
| 1 | 1 | 90 |
| 1 | 2 | NULL |
| 2 | 1 | 85 |
| 3 | 3 | 70 |
我們要選出所有符合以下條件的學生:
- 至少有選一門課
EXISTS。 - 至少有一門已選課沒有成績
IN。 - 屬於平均分數大於 80 的群組
HAVING。
用 EXISTS 跟 IN 解題
步驟 1:檢查有選課的學生(EXISTS)。 先從最簡單的條件開始。我們要知道哪些學生至少有選一門課。這時可以用 EXISTS。
SELECT name
FROM students s
WHERE EXISTS (
SELECT 1
FROM enrollments e
WHERE e.student_id = s.id
);
- 外層查詢從
students表選出名字。 - 子查詢檢查
enrollments表裡有沒有對應這個學生的紀錄(WHERE e.student_id = s.id)。 SELECT 1只是要確認有沒有資料,不在乎內容。
結果:
| name |
|---|
| Otto |
| Maria |
| Alex |
現在我們知道哪些學生有選課。不過我們還想更進一步,過濾掉那些沒有缺成績的學生。
步驟 2:檢查缺成績(IN + NULL)。 現在加個條件:只要有一門課沒成績的學生才要。這時 IN 跟 NULL 就派上用場啦。
SELECT name
FROM students s
WHERE id IN (
SELECT e.student_id
FROM enrollments e
WHERE e.grade IS NULL
);
- 外層查詢選出學生名字。
- 子查詢從
enrollments表找出grade IS NULL的student_id。
結果:
| name |
|---|
| Otto |
所以 Otto 是唯一有缺成績的學生,超戲劇化!但還沒結束,我們還要考慮只有平均分數大於 80 的群組。
用 HAVING 解題
步驟 3:分組跟 HAVING 過濾。
現在要把所有條件合起來。我們要:
- 算出每個群組的平均分數。
- 只留下平均分數大於 80 的群組。
- 選出這些群組裡、又符合前面條件的學生。
SELECT name
FROM students s
WHERE s.group_id IN (
SELECT group_id
FROM students
JOIN enrollments ON students.id = enrollments.student_id
WHERE grade IS NOT NULL
GROUP BY group_id
HAVING AVG(grade) > 80
)
AND id IN (
SELECT e.student_id
FROM enrollments e
WHERE e.grade IS NULL
);
- 外層查詢選出同時符合所有條件的學生名字。
- 第一個
WHERE子查詢回傳平均分數大於 80 的group_id。- 把
students跟enrollmentsjoin 起來拿到成績。 - 只看
grade IS NOT NULL的紀錄。 - 用
group_id分組。 - 用
HAVING過濾群組。
- 把
- 第二個
WHERE子查詢檢查學生有沒有缺成績的課。 - 兩個條件用
AND合併。
結果:
| name |
|---|
| Otto |
所以我們發現 Otto 不只是唯一缺成績的學生,還是那個超強群組的成員。
方法比較:EXISTS vs IN
EXISTS 最適合用來快速檢查有沒有資料。它很有效率,因為只要找到第一筆就停下來,對大表特別有用。
而 IN 比較適合你要根據一堆 id 去過濾資料的時候。不過要注意,如果子查詢回傳很多值,IN 可能會慢下來。
什麼時候用 HAVING
如果你要根據聚合結果(像是平均、總和)來過濾,HAVING 就是你的好朋友。但如果能把條件放到 WHERE(像是直接過濾欄位),查詢會更簡單也更快。
完整範例
再來一個例子加深印象:選出那些有學生分數低於 75,但沒有人選「哲學」課的群組。
再提醒一次,我們的表:
表格 students
| id | name | group_id |
|---|---|---|
| 1 | Otto | 101 |
| 2 | Maria | 101 |
| 3 | Alex | 102 |
| 4 | Anna | 103 |
表格 courses
| id | name |
|---|---|
| 1 | 數學 |
| 2 | 程式設計 |
| 3 | 哲學 |
表格 enrollments
| student_id | course_id | grade |
|---|---|---|
| 1 | 1 | 90 |
| 1 | 2 | NULL |
| 2 | 1 | 85 |
| 3 | 3 | 70 |
SELECT DISTINCT group_id
FROM students s
WHERE group_id IN (
SELECT s.group_id
FROM students s
JOIN enrollments e ON s.id = e.student_id
WHERE e.grade < 75
)
AND group_id NOT IN (
SELECT s.group_id -- 巢狀查詢第 1 層
FROM students s
JOIN enrollments e ON s.id = e.student_id
WHERE e.course_id = (
SELECT id FROM courses WHERE name = '哲學' -- 巢狀查詢第 2 層 :P
)
);
- 第一個子查詢選出有學生分數低於 75 的群組。
- 第二個子查詢排除有選「哲學」課的群組。
- 我們用
IN跟NOT IN組合條件,得到最終結果。
結果:
| group_id |
|---|
| 101 |
這有多實用?
現實生活中,這種技巧超級有用,尤其是你要分析複雜資料關係的時候。比如:
- 做數據分析時,找出「特殊」的客戶群(VIP、麻煩客戶等等)。
- 開發推薦系統時,要根據一堆條件過濾用戶。
- 面試時,考官要你優化一個很複雜的 SQL 查詢。
多練習吧!這就是你成為高手的路。
GO TO FULL VERSION