Table of Contents

在前兩篇文章裡,我們學會了 SQL 的四大基本指令 (SELECT、INSERT、UPDATE、DELETE) 以及多表查詢 (JOIN) 的各種技巧。接下來我們要進入 SQL 世界裡另外一個重要的主題,也就是聚合與統計

沒錯,當資料越來越龐大、資料表越來越多之後,如果這時候想要查「我想知道每個客戶的訂單總金額是多少?」「每種商品這個月賣了幾件?」「每個地區有多少位顧客?」這種時候,就會用到 GROUP BY 搭配聚合函數 (aggregate functions) 來幫你做總和、平均、筆數統計等操作了。

為什麼要用 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) 則把所有訂單金額加總。

執行後,就可以看到顧客 1001 買了三次,總共花了 2500元、1002和1003則是只買一次,分別花了900和2500 元:

這樣我們就能很快知道哪個顧客的消費最多,或哪個顧客的訂單數最多了。

範例 2:分月份統計銷售額,並只顯示高於 2000 的月份

假設我們的 orders 表有很多不同月份的訂單紀錄,想要看出「每個月」的銷售總額,並且只想要顯示「銷售總額大於 2000塊」的月份的話,要怎麼做呢?這時可以利用 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 時還是要把完整的算式寫出來
  • HAVINGWHERE 的差別:
    • WHERE 是針對「分組前」的資料行做條件過濾。
    • HAVING 是針對「分組後」的聚合結果進行過濾。(這個我後面會再細講)
  • ORDER BY ym 用來讓結果依照年月排序 (預設是升序)。

查詢後的結果可能長這樣:

表示 7、8、9 月的銷售額都超過 2000,其餘月份則因為銷售額不足 2000元 ,於是被排除在結果之外。

範例 3:統計每個商品類別的最高價、最低價、平均價

換個情境,假設我們有一張 products 表,存放每個商品的 product_id, category (商品類別), price 等資訊。我們想要知道:

  1. 每個類別 (category) 的最高價是多少?
  2. 最低價是多少?
  3. 平均價是多少?

就可以這樣寫:

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

所以要記住這兩者的差別很簡單,只要按照下面的順序思考,就不會搞混了:

  1. 第一步:用 WHERE 先過濾原始資料
  2. 第二步:用 GROUP BY 把資料分組
  3. 第三步:用 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)、或採用平行化的設計 (在更大規模的資料庫或數據倉儲場景) 來加速。這些更進階的優化,我們在後續文章再深入探討吧。