前一篇學過 GROUP BY 搭配聚合函數,可以算出每個客人的平均消費金額、每個部門的員工數量等等。
但有時候老闆不想看全部的結果。
比如說,老闆說:「資料太多了,你直接告訴我平均消費金額超過 10 萬的頂級客戶有哪些就好。」
這時候你需要在分組之後再做篩選,這就是 HAVING 的用途。
為什麼不能用 WHERE?
你可能會想:篩選不是用 WHERE 嗎?
問題是,WHERE 沒辦法篩選聚合函數的結果。
還記得前面提過,WHERE 在執行的時候,是一筆一筆資料往下看,判斷這筆資料要不要留下來。
WHERE 看的是「單筆資料的欄位值」,沒辦法看到「整組資料的聚合結果」。
比如說,你想篩選「平均金額超過 10 萬」的客人:
-- 這樣寫會報錯
SELECT 會員, AVG(金額)
FROM 訂單
WHERE AVG(金額) > 100000
GROUP BY 會員這樣寫會報錯,因為 WHERE 是一筆一筆資料往下看,判斷這筆資料要不要留下來。
但 AVG(金額) 是聚合函數,需要「一組資料」才能計算出結果。
WHERE 在看單筆資料的時候,根本沒辦法算出平均金額。
範例:找出平均消費金額超過 10 萬的頂級客戶
老闆說:「幫我找出平均消費金額超過 10 萬的頂級客戶,我要把這些人交給業務去經營。」
假設我們有一張訂單表:
| 訂單編號 | 會員 | 金額 | 訂單狀態 |
|---|---|---|---|
| 1 | Alice | 50000 | 成功 |
| 2 | Alice | 80000 | 成功 |
| 3 | Bob | 120000 | 成功 |
| 4 | Alice | 70000 | 成功 |
| 5 | Bob | 150000 | 成功 |
| 6 | Carol | 30000 | 成功 |
這時候要用 HAVING:
SELECT 會員, AVG(金額) AS 平均消費金額
FROM 訂單
WHERE 訂單狀態 = '成功'
GROUP BY 會員
HAVING AVG(金額) > 100000執行過程
第一步:FROM 訂單
先找到訂單表,拿到全部 6 筆資料。
第二步:WHERE 訂單狀態 = ‘成功’
篩選出訂單狀態是「成功」的資料,這裡全部都是成功,所以還是 6 筆。
第三步:GROUP BY 會員
把「會員欄位值相同的」放在一起:
| 組別 | 訂單編號 | 會員 | 金額 | 訂單狀態 |
|---|---|---|---|---|
| 第一組 | 1 | Alice | 50000 | 成功 |
| 第一組 | 2 | Alice | 80000 | 成功 |
| 第一組 | 4 | Alice | 70000 | 成功 |
| 第二組 | 3 | Bob | 120000 | 成功 |
| 第二組 | 5 | Bob | 150000 | 成功 |
| 第三組 | 6 | Carol | 30000 | 成功 |
然後把每一組壓縮成一列。
壓縮後,表面上只剩會員欄位:
| 會員 |
|---|
| Alice |
| Bob |
| Carol |
但資料庫背後還記得每一組有哪些金額:
- Alice 那一組:50000、80000、70000
- Bob 那一組:120000、150000
- Carol 那一組:30000
資料庫會暫時保留每一組的金額資料,讓聚合函數可以計算。
第四步:HAVING AVG(金額) > 100000
計算每一組的平均金額,然後篩選:
- Alice:(50000 + 80000 + 70000) / 3 = 66666,不符合條件,刪除
- Bob:(120000 + 150000) / 2 = 135000,符合條件,保留
- Carol:30000 / 1 = 30000,不符合條件,刪除
篩選後只剩 Bob。
第五步:SELECT 會員, AVG(金額) AS 平均消費金額
從篩選後的結果取出會員和平均金額。
這裡要注意:HAVING 只是用來「篩選」,它算出平均金額後,只是判斷要不要保留這一組,並沒有把結果存起來。
所以 SELECT 要顯示平均金額的話,還是要再寫一次 AVG(金額)。
最後輸出:
| 會員 | 平均消費金額 |
|---|---|
| Bob | 135000 |
這樣就找出平均消費金額超過 10 萬的頂級客戶了。
HAVING:專門篩選分組後的結果
從上面的範例可以看出,HAVING 就是用來篩選「分組後的聚合結果」。
HAVING 會在 GROUP BY 之後執行,這時候資料已經分組完成,聚合函數可以正常計算。
這段 SQL:
SELECT 會員, AVG(金額) AS 平均消費金額
FROM 訂單
WHERE 訂單狀態 = '成功'
GROUP BY 會員
HAVING AVG(金額) > 100000執行的順序是:
| 順序 | 指令 | 說明 |
|---|---|---|
| 1 | FROM | 找到訂單表 |
| 2 | WHERE | 篩選出成功的訂單 |
| 3 | GROUP BY | 按照會員分組 |
| 4 | HAVING | 計算每個會員的平均金額,只保留超過 10 萬的 |
| 5 | SELECT | 取出要顯示的欄位 |
| 6 | ORDER BY | 排序 |
HAVING 不能用 SELECT 的別名
注意:HAVING 裡面寫的是 AVG(金額),不是 SELECT 定義的別名 平均消費金額。
因為 HAVING 在 SELECT 之前執行,這時候別名還不存在。
-- 這樣寫在很多資料庫會報錯
SELECT 會員, AVG(金額) AS 平均消費金額
FROM 訂單
GROUP BY 會員
HAVING 平均消費金額 > 100000正確的寫法是在 HAVING 裡面重新寫一次聚合函數:
SELECT 會員, AVG(金額) AS 平均消費金額
FROM 訂單
GROUP BY 會員
HAVING AVG(金額) > 100000雖然有些資料庫(例如 MySQL)允許使用別名,但為了相容性,建議不要這樣做。
練習:找出平均工時超過 10 小時的員工
老闆說:「請告訴我平均工時超過 10 小時的員工,我要檢查是不是違反勞基法。」
遇到這種問題,可以先分析:
- 按照什麼分組?員工
- 要算什麼?平均工時
- 篩選條件?平均工時 > 10
因為篩選條件是「平均工時」,這是聚合函數的結果,所以要用 HAVING,不能用 WHERE。
SELECT 員工, AVG(工時) AS 平均工時
FROM 員工工時表
GROUP BY 員工
HAVING AVG(工時) > 10執行順序:
- FROM:找到員工工時表
- GROUP BY:按照員工分組
- HAVING:計算每個員工的平均工時,只保留超過 10 小時的
- SELECT:取出員工和平均工時
進階範例:需要子查詢的情況
有些問題用單一查詢沒辦法解決,需要用到子查詢。
範例:找出高級員工少於 10 人的部門有哪些員工
老闆說:「請告訴我高級員工少於 10 人的部門,然後列出這些部門的所有員工。」
這個問題有點複雜。
你可能會想:用 GROUP BY 按部門分組,用 HAVING 篩選出高級員工少於 10 人的部門,然後在 SELECT 裡面加上員工欄位,這樣不就好了嗎?
-- 這樣寫會報錯
SELECT 部門, 員工
FROM 部門員工表
WHERE 等級 = '高級'
GROUP BY 部門
HAVING COUNT(員工) < 10問題是,員工欄位沒有出現在 GROUP BY 裡,也沒有放在聚合函數裡,所以不能出現在 SELECT。
老闆要的是「這些部門的員工」,也就是個別員工的資料。
但 GROUP BY 之後,資料已經被壓縮了,你只能看到部門,看不到個別員工是誰。
所以這個問題沒辦法一次做完,需要分兩步:
- 先找出「高級員工少於 10 人的部門」
- 再找出「這些部門的員工」
第一步:找出高級員工少於 10 人的部門
SELECT 部門
FROM 部門員工表
WHERE 等級 = '高級'
GROUP BY 部門
HAVING COUNT(員工) < 10這段 SQL 的執行順序:
- FROM:找到部門員工表
- WHERE:篩選出等級是「高級」的員工
- GROUP BY:按照部門分組
- HAVING:計算每個部門的高級員工數量,只保留少於 10 人的
- SELECT:取出部門
這裡只 SELECT 部門,因為我們只需要知道「哪些部門」符合條件,等一下要拿這個結果去做篩選。
第二步:找出這些部門的員工
SELECT 員工, 部門
FROM 部門員工表
WHERE 部門 IN (第一步的結果)這段 SQL 用 IN 來篩選:只要員工的部門出現在第一步的結果裡,就保留下來。
合併起來:
SELECT 員工, 部門
FROM 部門員工表
WHERE 部門 IN (
SELECT 部門
FROM 部門員工表
WHERE 等級 = '高級'
GROUP BY 部門
HAVING COUNT(員工) < 10
)這裡用到了子查詢(括號裡面的 SELECT),把第一步的結果當作第二步的篩選條件。
子查詢放在 IN 後面,會先執行完,產生一個部門清單,然後外層的查詢再用這個清單來篩選。
進階範例:兩層聚合運算
有些問題需要做兩層聚合運算。
範例:找出最高日銷售額低於 30 萬的分店
老闆說:「請告訴我最高日銷售額低於 30 萬的分店,這些店業績太差了。」
假設我們有一張分店交易表:
| 交易編號 | 分店 | 交易日期 | 金額 |
|---|---|---|---|
| 1 | 大直店 | 2024-04-01 | 50000 |
| 2 | 大直店 | 2024-04-01 | 80000 |
| 3 | 大直店 | 2024-04-02 | 120000 |
| … | … | … | … |
問題是,表裡面沒有「日銷售額」這個欄位,只有每一筆交易的金額。
所以要分兩步:
- 先算出每間分店的每一天的日銷售額
- 再找出每間分店的最高日銷售額,並篩選低於 30 萬的
第一步:算出每間分店每天的日銷售額
SELECT 分店, 交易日期, SUM(金額) AS 日銷售額
FROM 分店交易表
GROUP BY 分店, 交易日期這段 SQL 的執行順序:
- FROM:找到分店交易表
- GROUP BY:按照「分店 + 交易日期」的組合分組
- SELECT:取出分店、交易日期,並用 SUM 計算每一組的金額總和
這樣會得到每間分店每天的日銷售額,例如:
| 分店 | 交易日期 | 日銷售額 |
|---|---|---|
| 大直店 | 2024-04-01 | 130000 |
| 大直店 | 2024-04-02 | 120000 |
| 內湖店 | 2024-04-01 | 250000 |
| 內湖店 | 2024-04-02 | 280000 |
第二步:找出最高日銷售額低於 30 萬的分店
SELECT 分店, MAX(日銷售額) AS 最高日銷售額
FROM (第一步的結果) AS 分店日銷售額
GROUP BY 分店
HAVING MAX(日銷售額) < 300000這段 SQL 的執行順序:
- FROM:從第一步的結果取得資料
- GROUP BY:按照分店分組
- HAVING:計算每間分店的最高日銷售額,只保留低於 30 萬的
- SELECT:取出分店和最高日銷售額
這裡的 FROM 後面放的是第一步的查詢結果,所以要加上別名 AS 分店日銷售額。
合併起來:
SELECT 分店, MAX(日銷售額) AS 最高日銷售額
FROM (
SELECT 分店, 交易日期, SUM(金額) AS 日銷售額
FROM 分店交易表
GROUP BY 分店, 交易日期
) AS 分店日銷售額
GROUP BY 分店
HAVING MAX(日銷售額) < 300000這裡用到了兩層聚合:
- 內層:GROUP BY 分店和交易日期,用 SUM 算出日銷售額
- 外層:GROUP BY 分店,用 MAX 找出最高日銷售額,再用 HAVING 篩選
當你看到這種複雜的 SQL,可以先把子查詢折起來不看,先看外層的結構,再看內層在做什麼。
小結
這篇文章介紹了 HAVING:
- 為什麼需要 HAVING:WHERE 沒辦法篩選聚合函數的結果,需要用 HAVING
- WHERE 和 HAVING 的差別:WHERE 在 GROUP BY 之前執行,HAVING 在 GROUP BY 之後執行
- 執行順序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
- 進階應用:有些問題需要子查詢或兩層聚合運算才能解決