本文 AI 產出,尚未審核
MySQL – 聚合函式與分組
主題:HAVING 過濾分組資料
簡介
在資料庫查詢中,我們常常需要先 將資料依某個欄位分組,再針對每個組別計算總和、平均值、最大值等聚合結果。GROUP BY 能夠完成「分組」的工作,但如果只想保留 符合特定條件的組別,單靠 WHERE 已經不夠,因為 WHERE 是在分組 之前 進行過濾。
此時就需要 HAVING 子句。HAVING 允許我們在 聚合計算完成後 再篩選組別,讓查詢結果更精準且效能更好。對於報表、統計分析、商業智慧等應用,掌握 HAVING 的使用是必備技能。
核心概念
1. HAVING 與 WHERE 的差別
| 條件 | 執行階段 | 作用對象 |
|---|---|---|
WHERE |
分組前 | 原始列 (row) |
HAVING |
分組後 | 聚合結果 (如 SUM(), COUNT() …) |
簡單說:
WHERE篩選單筆資料,HAVING篩選「一群資料」的統計結果。
2. 基本語法
SELECT <欄位列表>, <聚合函式>(<欄位>) AS <別名>
FROM <資料表>
WHERE <前置條件> -- (可選) 先過濾原始列
GROUP BY <分組欄位>
HAVING <聚合條件> -- 必須使用聚合函式或別名
ORDER BY <排序欄位>;
HAVING可以使用聚合函式,也可以直接使用在SELECT中定義的別名。- 多個條件可使用
AND、OR串接,寫法與WHERE完全相同。
3. 常見的聚合函式
| 函式 | 說明 |
|---|---|
COUNT(*) |
計算組內列數 |
SUM(col) |
計算組內數值總和 |
AVG(col) |
計算組內平均值 |
MAX(col) / MIN(col) |
取得最大 / 最小值 |
程式碼範例
以下示範使用 MySQL 8.0,資料表 orders 結構簡化如下:
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
order_date DATE,
amount DECIMAL(10,2)
);
範例 1:找出 每位客戶 的總消費金額,且只保留總金額 大於 5000 的客戶
SELECT
customer_id,
SUM(amount) AS total_spent
FROM orders
GROUP BY customer_id
HAVING total_spent > 5000;
說明:
SUM(amount)先算出每個customer_id的消費總額,HAVING再把小於 5000 的組別剔除。
範例 2:統計 每個月份 的訂單筆數,僅顯示 筆數不少於 10 的月份
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
COUNT(*) AS order_cnt
FROM orders
GROUP BY month
HAVING order_cnt >= 10
ORDER BY month;
技巧:使用
DATE_FORMAT產生月份字串作為分組依據,HAVING直接使用order_cnt別名。
範例 3:找出 平均訂單金額 超過 1000 的客戶,同時篩除訂單筆數少於 5 的客戶
SELECT
customer_id,
AVG(amount) AS avg_amount,
COUNT(*) AS order_cnt
FROM orders
GROUP BY customer_id
HAVING avg_amount > 1000 AND order_cnt >= 5;
重點:
HAVING可同時檢查多個聚合條件,條件之間使用AND或OR。
範例 4:使用子查詢結合 HAVING,取得 每個客戶 最近一年內的 最高單筆金額,且該金額 超過 2000
SELECT
customer_id,
MAX(amount) AS max_amount
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 1 YEAR)
GROUP BY customer_id
HAVING max_amount > 2000;
說明:先以
WHERE限制時間範圍,再以HAVING檢查聚合結果。
範例 5:結合 ROLLUP 產生小計與總計,並只保留 金額總和 大於 10000 的層級
SELECT
IFNULL(customer_id, 'TOTAL') AS cust,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id WITH ROLLUP
HAVING total_amount > 10000;
提示:
ROLLUP會產生額外的「小計」與「總計」列,HAVING同樣適用於這些衍生列。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的解決方式 |
|---|---|---|
忘記加 GROUP BY |
HAVING 必須與 GROUP BY 搭配使用,否則會產生錯誤或不符合預期。 |
確認查詢邏輯真的需要分組,再加入 GROUP BY。 |
在 HAVING 使用非聚合欄位 |
雖然 MySQL 允許,但會產生不確定結果,因為非聚合欄位在分組後可能有多個值。 | 僅在 HAVING 使用聚合函式或 SELECT 別名。 |
WHERE 與 HAVING 重複條件 |
把可以在 WHERE 做的過濾寫在 HAVING,會造成不必要的聚合運算,降低效能。 |
把可在資料列層面過濾的條件放到 WHERE,只在需要聚合結果時才用 HAVING。 |
| 忘記別名或函式重複 | 在 HAVING 中直接寫 SUM(col) > 0,但 SELECT 也用了相同的聚合,易造成閱讀困難。 |
為聚合結果取有意義的別名,然後在 HAVING 使用別名,提升可讀性。 |
使用 HAVING 產生全表掃描 |
若條件過於寬鬆,會把所有分組都算出來再過濾,效能不佳。 | 盡量在 WHERE 先縮小資料範圍,或使用適當的索引。 |
最佳實踐小結
- 先用
WHERE篩選,再用GROUP BY分組,最後用HAVING濾組。 - 為聚合結果取別名,在
HAVING中直接使用別名,讓 SQL 更易讀。 - 檢查執行計畫(
EXPLAIN),確保WHERE條件被索引利用,避免不必要的全表掃描。 - 避免在
HAVING中使用非聚合欄位,除非確定該欄位在每個組內值唯一。
實際應用場景
| 場景 | 需求 | HAVING 的角色 |
|---|---|---|
| 銷售報表 | 列出每月營收超過目標的月份 | 先 GROUP BY 月份,HAVING 只保留營收 > 目標 |
| 客戶分層 | 找出購買次數達 10 次且平均金額 > 2000 的 VIP 客戶 | GROUP BY customer_id,HAVING 同時檢查 COUNT 與 AVG |
| 庫存警示 | 列出庫存量低於安全庫存且最近 30 天出貨量超過 100 的商品 | 先 WHERE 限制日期,GROUP BY product_id,HAVING 同時檢查 SUM(quantity) 與 stock |
| 網站流量分析 | 只關注每日訪問次數超過 5000 的日期 | GROUP BY DATE(access_time),HAVING 篩選 COUNT(*) > 5000 |
| 金融風控 | 找出信用卡持卡人中,該月消費金額超過 5 萬且交易次數超過 20 筆的高風險客戶 | GROUP BY card_no, month,HAVING 同時檢查 SUM(amount) 與 COUNT(*) |
總結
HAVING是 在聚合後過濾組別 的關鍵子句,與WHERE互補。- 正確的查詢順序是:
WHERE→GROUP BY→HAVING→SELECT→ORDER BY。 - 透過別名、適當的索引與前置過濾,可讓
HAVING的使用既 高效 又 易讀。 - 在實務上,
HAVING常被用於 報表、分層、警示 等需要根據統計結果進一步篩選的情境。
掌握了 HAVING,你就能在 MySQL 中寫出更具彈性、效能更佳的資料分析查詢,為業務決策提供可靠的數據支撐。祝你在 MySQL 的世界裡玩得開心,查詢寫得順手!