在前兩篇文章裡,我們學會了 SQL 的四大基本指令 (SELECT、INSERT、UPDATE、DELETE) 以及多表查詢 (JOIN) 的各種技巧。接下來,我們要進入 SQL 世界裡另外一個重要的主題,也就是聚合與統計。
沒錯,當你的資料越來越龐大、資料表越來越多之後,你肯定想過:「我想知道每個客戶的訂單總金額是多少?」「每種商品這個月賣了幾件?」「每個地區有多少位顧客?」這種時候,就會用到 GROUP BY 搭配聚合函數 (aggregate functions) 來幫你做總和、平均、筆數統計等操作了。
這篇文章將帶你從基礎觀念開始,一路到實務範例,再深入幾個常見的注意事項。最後,我會用 3 個小情境範例幫你一次串起來,讓你對 GROUP BY 的使用更加熟悉喔。
為什麼要用 GROUP BY?
GROUP BY 是用來把資料「依照某個(或多個) 欄位的值」分組,然後對每組資料進行聚合運算。比如說,我們可以用來:
- 計算每個商品類別的總銷售金額 (SUM)
- 統計每個城市有多少位顧客 (COUNT)
- 找出各類別的平均訂單金額 (AVG)
- 以及其他更多運用,如 MIN、MAX 等
換句話說,如果你想要達到「以某欄位作為分組、再做總計」的結果,GROUP BY 是一定要學會的。
認識常用的聚合函數 (Aggregate Functions)
在使用 GROUP BY 前,先來複習幾個常見的聚合函數,因為它們跟 GROUP BY 是絕對的好搭檔:
- COUNT(*)、COUNT(欄位):用來計算某個群組中的資料筆數。
- SUM(欄位):將指定欄位 (通常是數字型) 加總。
- AVG(欄位):計算該欄位的平均值。
- MIN(欄位)、MAX(欄位):找出群組中該欄位的最小值 (MIN) 或最大值 (MAX)。
有了這些聚合函數之後,再配合 GROUP BY,就可以對每個分組做出計算了。(比如「按顧客 ID 分組,算出每個顧客的訂單總數」等等)
GROUP BY 基本語法
一個最簡單的 GROUP BY 查詢大概長這樣:
SELECT 欄位1, 聚合函數(欄位2)
FROM 資料表
WHERE 條件 ...
GROUP BY 欄位1;
- SELECT 裡面出現的非聚合欄位,必須也要出現在 GROUP BY 中。
- 例如你想要顯示 city 和「每個城市有多少用戶 (COUNT)」,就得在 GROUP BY city。
- 不可以寫出「SELECT city, user_name, COUNT(*) FROM … GROUP BY city」,但同時又不把 user_name 放在 GROUP BY 裡,否則在某些資料庫 (像 MySQL 的舊模式除外) 會出錯。
- WHERE 能先過濾不要的資料行,再送進聚合處理。
- GROUP BY 是正式把資料「分組」。每個組別裡會以指定欄位 (或欄位組) 來區分。
- HAVING (等會再詳述) 用來針對「分組完」的結果再次過濾。它有點像是「針對聚合後結果」的 WHERE。
3 個實用範例帶你上手
範例 1:統計每位顧客的訂單筆數與總金額
想像你有一張 orders 表,裡面紀錄了:
-- orders
order_id | customer_id | amount | order_date
---------+------------ +---------+-------------
1 | 1001 | 1200 | 2019-08-10
2 | 1001 | 800 | 2019-08-11
3 | 1002 | 900 | 2019-08-12
4 | 1001 | 500 | 2019-08-15
5 | 1003 | 2500 | 2019-09-01
如果我們想要知道「每一位顧客 (customer_id)」,他在這張 orders 表裡面總共有幾筆訂單、訂單金額合計是多少,語法就會像這樣:
SELECT
customer_id,
COUNT(*) AS total_orders, -- 統計該顧客有幾筆訂單
SUM(amount) AS total_amount -- 統計該顧客的訂單金額加總
FROM orders
GROUP BY customer_id;
- GROUP BY customer_id 代表以 customer_id 來分組。
- 每個 customer_id 所「歸屬」的所有紀錄,會被打包成一組進行運算。
- COUNT(*) 會把該顧客的所有訂單筆數給算出來,SUM(amount) 則把所有訂單金額加總。
執行後,你可能會得到類似以下結果 (依實際資料而定):
這樣,你就能很快知道哪個顧客的消費最多,或哪個顧客的訂單數最多。
範例 2:分月份統計銷售額,並只顯示高於 2,000 的月份
假設我們的 orders 表有很多不同月份的訂單紀錄,想要看出「每個月」的銷售總額,並且只想要顯示「銷售總額大於 2,000」的月份的話,要怎麼做呢?這時可以利用 DATE 函數配合 GROUP BY,再用 HAVING 過濾:
SELECT
EXTRACT(YEAR_MONTH FROM order_date) AS ym, -- 取出年月 (依不同資料庫語法可能不同)
SUM(amount) AS monthly_sales
FROM orders
GROUP BY EXTRACT(YEAR_MONTH FROM order_date)
HAVING SUM(amount) > 2000
ORDER BY ym;
- 不同的資料庫系統在提取日期的年月時會用不同的語法:
- 如果你用 MySQL,要寫:EXTRACT(YEAR_MONTH FROM order_date)
- 如果你用 PostgreSQL 或 Oracle,要寫:TO_CHAR(order_date, 'YYYYMM')
- GROUP BY 時要寫出完整的運算式 EXTRACT(YEAR_MONTH FROM order_date),不能只寫別名 ym。為什麼呢?
- SQL 的規則要求:GROUP BY 要跟 SELECT 用一樣的算式,不能用別名
- 所以即使我們在 SELECT 中把 EXTRACT(YEAR_MONTH FROM order_date) 命名為 ym,在 GROUP BY 時還是要把完整的算式寫出來
- HAVING 與 WHERE 的差別:
- WHERE 是針對「分組前」的資料行做條件過濾。
- HAVING 是針對「分組後」的聚合結果進行過濾。(這個我後面會再細講)
- ORDER BY ym 用來讓結果依照年月排序 (預設是升序)。
查詢後的結果可能長這樣:
表示 7、8、9 月的銷售額都超過 2,000,其餘月份則因為銷售額不足 2,000 被排除在外。
範例 3:統計每個商品類別的最高價、最低價、平均價
換個情境,假設我們有一張 products 表,存放每個商品的 product_id, category (商品類別), price 等資訊。我們想要知道:
- 每個類別 (category) 的最高價是多少?
- 最低價是多少?
- 平均價是多少?
就可以這樣寫:
SELECT
category,
MAX(price) AS max_price,
MIN(price) AS min_price,
AVG(price) AS avg_price
FROM products
GROUP BY category;
假設你的商品類別像是 Electronics, Clothing, Furniture 等,執行後就能快速看出各類別的價格分布範圍。這個在商品定價策略、促銷策略上都滿實用。
WHERE 與 HAVING 的差別
WHERE 跟 HAVING 都是用來過濾資料的指令,很多人常常搞不清楚該用哪一個。其實差別就在於,我們想要在 GROUP BY 這個動作發生之前還是之後過濾資料:
WHERE:
它會在資料分組前先過濾資料
舉個例子:如果你想找出價格超過 100 元的商品,就用 WHERE
HAVING:
它是在資料分組後才進行過濾
比如說:想找出「銷售總額超過 2000 元」的商品類別,就要用 HAVING
所以要記住這兩者的差別很簡單,只要按照下面的順序思考,就不會搞混了:
- 第一步:用 WHERE 先過濾原始資料
- 第二步:用 GROUP BY 把資料分組
- 第三步:用 HAVING 過濾分組後的結果
常見問題與注意事項
SELECT 的欄位設定是有規則的
- 沒被聚合函數包住 (如 SUM, COUNT, MAX…) 的欄位,一定也要出現在GROUP BY 裡,否則大多數資料庫會報錯。
- MySQL 傳統模式 (sql_mode 關閉 ONLY_FULL_GROUP_BY) 可能不會擋,但出來的結果通常也不會是我們想要的。
NULL 值在 GROUP BY 裡的處理
- 如果某欄位有 NULL,這些 NULL 會被當成同一組,但大多數情況下要注意你的應用邏輯是否需要排除 NULL 或加上條件。
GROUP BY 多個欄位
- 你可以一次指定多個欄位分組,像是 GROUP BY category, brand,會先依照 category,再細分 brand 進行分組。
ORDER BY 也可以排序聚合欄位
- 如果你想依照分組結果做排序,例如 ORDER BY SUM(amount) DESC,你可以在 SELECT 裡面對那個聚合結果取個別名,再利用別名排序即可。比如:
SELECT category, SUM(price) AS total_price
FROM products
GROUP BY category
ORDER BY total_price DESC;
如果要 WHERE 與 HAVING 同時用
- 可以先用 WHERE 過濾掉你不要的紀錄,再用 GROUP BY 分組,最後再用 HAVING 過濾不想要的分組。
- 請記得「條件寫錯位置,會讓最終結果天差地遠」。
小結:Group By 讓你抓出資料背後的意義
掌握了 GROUP BY 的感覺,就像把大海撈針變簡單一樣。
- 現在你能用一條敘述就統計出所有客戶的訂單總數或最大值、最小值了
- 你能針對不同的區間 (月份、季度、年) 分別去計算平均值,然後再用 HAVING 去做更進一步篩選了
- 你也能用多欄位分組,做出「某個城市+某個商品類別」這種複雜的綜合統計報表了。
若說前一篇講的 JOIN 讓你能跨表整合資料,那 GROUP BY 就是幫你把大量資料整理出洞見的關鍵。如果你對資料分析、商業智慧 (BI) 有興趣,GROUP BY 更是一個基礎中的基礎。好好熟悉它,在撰寫報表或製作儀表板時會非常有用喔。
未來在執行效能上,你也會發現:大量的 GROUP BY 查詢可能很吃資源,需要適度建索引 (Index)、或採用平行化的設計 (在更大規模的資料庫或數據倉儲場景) 來加速。這些更進階的優化,我們在後續文章再深入探討吧。