CodeGym /課程 /SQL SELF /複雜巢狀查詢範例:結合 EXISTS、IN、HAVING

複雜巢狀查詢範例:結合 EXISTS、IN、HAVING

SQL SELF
等級 14 , 課堂 3
開放

恭喜啦,我們終於來到真正有趣的地方!今天要來看看怎麼把不同類型的子查詢混在一起用,解決一些比較 tricky 的問題。EXISTSINHAVING 這三個組合起來,你就像資料庫魔法師一樣。等等會從一張表撈資料、根據另一張表的資料過濾、分組,然後再過濾分組結果。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

我們要選出所有符合以下條件的學生:

  1. 至少有選一門課 EXISTS
  2. 至少有一門已選課沒有成績 IN
  3. 屬於平均分數大於 80 的群組 HAVING

EXISTSIN 解題

步驟 1:檢查有選課的學生(EXISTS)。 先從最簡單的條件開始。我們要知道哪些學生至少有選一門課。這時可以用 EXISTS

SELECT name
FROM students s
WHERE EXISTS (
  SELECT 1
  FROM enrollments e
  WHERE e.student_id = s.id
);
  1. 外層查詢從 students 表選出名字。
  2. 子查詢檢查 enrollments 表裡有沒有對應這個學生的紀錄(WHERE e.student_id = s.id)。
  3. SELECT 1 只是要確認有沒有資料,不在乎內容。

結果:

name
Otto
Maria
Alex

現在我們知道哪些學生有選課。不過我們還想更進一步,過濾掉那些沒有缺成績的學生。

步驟 2:檢查缺成績(IN + NULL)。 現在加個條件:只要有一門課沒成績的學生才要。這時 INNULL 就派上用場啦。

SELECT name
FROM students s
WHERE id IN (
  SELECT e.student_id
  FROM enrollments e
  WHERE e.grade IS NULL
);
  1. 外層查詢選出學生名字。
  2. 子查詢從 enrollments 表找出 grade IS NULLstudent_id

結果:

name
Otto

所以 Otto 是唯一有缺成績的學生,超戲劇化!但還沒結束,我們還要考慮只有平均分數大於 80 的群組。

HAVING 解題

步驟 3:分組跟 HAVING 過濾。

現在要把所有條件合起來。我們要:

  1. 算出每個群組的平均分數。
  2. 只留下平均分數大於 80 的群組。
  3. 選出這些群組裡、又符合前面條件的學生。
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
);
  1. 外層查詢選出同時符合所有條件的學生名字。
  2. 第一個 WHERE 子查詢回傳平均分數大於 80 的 group_id
    • studentsenrollments join 起來拿到成績。
    • 只看 grade IS NOT NULL 的紀錄。
    • group_id 分組。
    • HAVING 過濾群組。
  3. 第二個 WHERE 子查詢檢查學生有沒有缺成績的課。
  4. 兩個條件用 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
  )
);
  1. 第一個子查詢選出有學生分數低於 75 的群組。
  2. 第二個子查詢排除有選「哲學」課的群組。
  3. 我們用 INNOT IN 組合條件,得到最終結果。

結果:

group_id
101

這有多實用?

現實生活中,這種技巧超級有用,尤其是你要分析複雜資料關係的時候。比如:

  • 做數據分析時,找出「特殊」的客戶群(VIP、麻煩客戶等等)。
  • 開發推薦系統時,要根據一堆條件過濾用戶。
  • 面試時,考官要你優化一個很複雜的 SQL 查詢。

多練習吧!這就是你成為高手的路。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION