Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

本文為「SQL 資料庫新手村」系列第 40 篇

SQL SELECT 入門:用 HAVING 篩選分組後的結果

最後更新:2026年1月23日資料庫

前一篇學過 GROUP BY 搭配聚合函數,可以算出每個客人的平均消費金額、每個部門的員工數量等等。

但有時候老闆不想看全部的結果。

比如說,老闆說:「資料太多了,你直接告訴我平均消費金額超過 10 萬的頂級客戶有哪些就好。」

這時候你需要在分組之後再做篩選,這就是 HAVING 的用途。

為什麼不能用 WHERE?

你可能會想:篩選不是用 WHERE 嗎?

問題是,WHERE 沒辦法篩選聚合函數的結果。

還記得前面提過,WHERE 在執行的時候,是一筆一筆資料往下看,判斷這筆資料要不要留下來。

WHERE 看的是「單筆資料的欄位值」,沒辦法看到「整組資料的聚合結果」。

比如說,你想篩選「平均金額超過 10 萬」的客人:

-- 這樣寫會報錯
SELECT 會員, AVG(金額)
FROM 訂單
WHERE AVG(金額) > 100000
GROUP BY 會員

這樣寫會報錯,因為 WHERE 是一筆一筆資料往下看,判斷這筆資料要不要留下來。

但 AVG(金額) 是聚合函數,需要「一組資料」才能計算出結果。

WHERE 在看單筆資料的時候,根本沒辦法算出平均金額。

範例:找出平均消費金額超過 10 萬的頂級客戶

老闆說:「幫我找出平均消費金額超過 10 萬的頂級客戶,我要把這些人交給業務去經營。」

假設我們有一張訂單表:

訂單編號會員金額訂單狀態
1Alice50000成功
2Alice80000成功
3Bob120000成功
4Alice70000成功
5Bob150000成功
6Carol30000成功
會員Alice
金額50000
訂單狀態成功
會員Alice
金額80000
訂單狀態成功
會員Bob
金額120000
訂單狀態成功
會員Alice
金額70000
訂單狀態成功
會員Bob
金額150000
訂單狀態成功
會員Carol
金額30000
訂單狀態成功

這時候要用 HAVING:

SELECT 會員, AVG(金額) AS 平均消費金額
FROM 訂單
WHERE 訂單狀態 = '成功'
GROUP BY 會員
HAVING AVG(金額) > 100000

執行過程

第一步:FROM 訂單

先找到訂單表,拿到全部 6 筆資料。

第二步:WHERE 訂單狀態 = ‘成功’

篩選出訂單狀態是「成功」的資料,這裡全部都是成功,所以還是 6 筆。

第三步:GROUP BY 會員

把「會員欄位值相同的」放在一起:

組別訂單編號會員金額訂單狀態
第一組1Alice50000成功
第一組2Alice80000成功
第一組4Alice70000成功
第二組3Bob120000成功
第二組5Bob150000成功
第三組6Carol30000成功
訂單編號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(金額)。

最後輸出:

會員平均消費金額
Bob135000
平均消費金額135000

這樣就找出平均消費金額超過 10 萬的頂級客戶了。

HAVING:專門篩選分組後的結果

從上面的範例可以看出,HAVING 就是用來篩選「分組後的聚合結果」。

HAVING 會在 GROUP BY 之後執行,這時候資料已經分組完成,聚合函數可以正常計算。

這段 SQL:

SELECT 會員, AVG(金額) AS 平均消費金額
FROM 訂單
WHERE 訂單狀態 = '成功'
GROUP BY 會員
HAVING AVG(金額) > 100000

執行的順序是:

順序指令說明
1FROM找到訂單表
2WHERE篩選出成功的訂單
3GROUP BY按照會員分組
4HAVING計算每個會員的平均金額,只保留超過 10 萬的
5SELECT取出要顯示的欄位
6ORDER BY排序
指令FROM
說明找到訂單表
指令WHERE
說明篩選出成功的訂單
指令GROUP BY
說明按照會員分組
指令HAVING
說明計算每個會員的平均金額,只保留超過 10 萬的
指令SELECT
說明取出要顯示的欄位
指令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

執行順序:

  1. FROM:找到員工工時表
  2. GROUP BY:按照員工分組
  3. HAVING:計算每個員工的平均工時,只保留超過 10 小時的
  4. SELECT:取出員工和平均工時

進階範例:需要子查詢的情況

有些問題用單一查詢沒辦法解決,需要用到子查詢。

範例:找出高級員工少於 10 人的部門有哪些員工

老闆說:「請告訴我高級員工少於 10 人的部門,然後列出這些部門的所有員工。」

這個問題有點複雜。

你可能會想:用 GROUP BY 按部門分組,用 HAVING 篩選出高級員工少於 10 人的部門,然後在 SELECT 裡面加上員工欄位,這樣不就好了嗎?

-- 這樣寫會報錯
SELECT 部門, 員工
FROM 部門員工表
WHERE 等級 = '高級'
GROUP BY 部門
HAVING COUNT(員工) < 10

問題是,員工欄位沒有出現在 GROUP BY 裡,也沒有放在聚合函數裡,所以不能出現在 SELECT。

老闆要的是「這些部門的員工」,也就是個別員工的資料。

但 GROUP BY 之後,資料已經被壓縮了,你只能看到部門,看不到個別員工是誰。

所以這個問題沒辦法一次做完,需要分兩步:

  1. 先找出「高級員工少於 10 人的部門」
  2. 再找出「這些部門的員工」

第一步:找出高級員工少於 10 人的部門

SELECT 部門
FROM 部門員工表
WHERE 等級 = '高級'
GROUP BY 部門
HAVING COUNT(員工) < 10

這段 SQL 的執行順序:

  1. FROM:找到部門員工表
  2. WHERE:篩選出等級是「高級」的員工
  3. GROUP BY:按照部門分組
  4. HAVING:計算每個部門的高級員工數量,只保留少於 10 人的
  5. 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-0150000
2大直店2024-04-0180000
3大直店2024-04-02120000
…………
分店大直店
交易日期2024-04-01
金額50000
分店大直店
交易日期2024-04-01
金額80000
分店大直店
交易日期2024-04-02
金額120000
分店…
交易日期…
金額…

問題是,表裡面沒有「日銷售額」這個欄位,只有每一筆交易的金額。

所以要分兩步:

  1. 先算出每間分店的每一天的日銷售額
  2. 再找出每間分店的最高日銷售額,並篩選低於 30 萬的

第一步:算出每間分店每天的日銷售額

SELECT 分店, 交易日期, SUM(金額) AS 日銷售額
FROM 分店交易表
GROUP BY 分店, 交易日期

這段 SQL 的執行順序:

  1. FROM:找到分店交易表
  2. GROUP BY:按照「分店 + 交易日期」的組合分組
  3. SELECT:取出分店、交易日期,並用 SUM 計算每一組的金額總和

這樣會得到每間分店每天的日銷售額,例如:

分店交易日期日銷售額
大直店2024-04-01130000
大直店2024-04-02120000
內湖店2024-04-01250000
內湖店2024-04-02280000
交易日期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 的執行順序:

  1. FROM:從第一步的結果取得資料
  2. GROUP BY:按照分店分組
  3. HAVING:計算每間分店的最高日銷售額,只保留低於 30 萬的
  4. 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:

  1. 為什麼需要 HAVING:WHERE 沒辦法篩選聚合函數的結果,需要用 HAVING
  2. WHERE 和 HAVING 的差別:WHERE 在 GROUP BY 之前執行,HAVING 在 GROUP BY 之後執行
  3. 執行順序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
  4. 進階應用:有些問題需要子查詢或兩層聚合運算才能解決
上一篇SQL SELECT 入門:GROUP BY 搭配聚合函數
目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

資料庫

目錄

  • 為什麼不能用 WHERE?
  • 範例:找出平均消費金額超過 10 萬的頂級客戶
  • 執行過程
  • HAVING:專門篩選分組後的結果
  • HAVING 不能用 SELECT 的別名
  • 練習:找出平均工時超過 10 小時的員工
  • 進階範例:需要子查詢的情況
  • 範例:找出高級員工少於 10 人的部門有哪些員工
  • 進階範例:兩層聚合運算
  • 範例:找出最高日銷售額低於 30 萬的分店
  • 小結