《MySQL 入門教程》第 12 篇 分組統計

group by

12.1 聚合函數

在 SQL 中,聚合函數(Aggregate Function)用於對一組數據進行彙總計算,並且返回單個分析結果。例如,公司中的員工總數、所有員工的平均月薪等。MySQL 中常見的聚合函數包括:

  • COUNT,返回查詢結果的行數;
  • AVG,計算一組數值的平均值;
  • SUM,計算一組數值的總和;
  • MAX ,計算一組數據中的最大值;
  • MIN,計算一組數據中的最小值;
  • GROUP_CONCAT,連接一組字符串。

例如,以下查詢返回了公司中的員工總數、平均月薪、最高月薪、最低月薪以及所有員工的月薪總和:

select count(*) "員工數量",
       avg(salary) "平均月薪",
       max(salary) "最高月薪",
       min(salary) "最低月薪",
       sum(salary) "月薪總和"
from employee;
員工數量|平均月薪     |最高月薪 |最低月薪 |月薪總和  |
-------|-----------|--------|-------|---------|
     25|9912.000000|30000.00|4000.00|247800.00|

以下查詢返回了行政管理部所有員工的姓名組成的字符串:

select group_concat(emp_name) "所有員工",
       group_concat(emp_name order by salary separator ':') "所有員工"
from employee
where dept_id = 1;
所有員工     |所有員工      |
------------|------------|
劉備,關羽,張飛|張飛:關羽:劉備|

第一個 group_concat 函數使用默認的參數和分隔符,第二個 group_concat 函數指定了字符串的連接順序和分隔符。

使用聚合函數時需要注意兩點:

  • 在聚合函數的參數中加上 DISTINCT 關鍵字,可以在計算之前排除重複值。例如,當 AVG 函數中包含 DISTINCT 參數時,在計算平均值之前會排除掉重複值。因此,(1、1、2)的平均值爲 (1 + 2) / 2 = 1.5,而不是 (1 + 1 + 2) / 3 = 1.33。
  • 聚合函數在計算時,忽略輸入值爲 NULL 的數據行;COUNT(*) 除外。例如,當 AVG 函數中存在空值時,計算之前會忽略這些空值。因此,(1,2,NULL)的平均值爲 (1 + 2) / 2 = 1.5,而不是 (1 + 2) / 3 = 1。

例如:

select count(*), 
       count(distinct sex), 
       count(bonus)
from employee;
count(*)|count(distinct sex)|count(bonus)|
--------|-------------------|------------|
      25|                  2|           9|

其中,COUNT(*) 返回了員工的總數;count(distinct sex) 返回了不同性別的種類(男、女);count(bonus) 返回了擁有獎金的員工數量,只有 9 名員工有獎金。

聚合函數的完整語法如下:

aggregate_function( [ALL | DISTINCT] expression)

其中,ALL 表示計算時不排除重複值。這是默認行爲,通常省略。

📝MySQL 還支持更多的聚合函數,例如計算方差和標準差的 VAR_SAMP 和 STDDEV_SAMP 函數;詳細列表可以參考官方文檔

聚合函數單獨使用時,只能返回所有數據的整體彙總結果。如果我們想要按照不同的分組進行統計,例如按照部門統計員工的平均薪水、員工數量等,就要將聚合函數和GROUP BY分組子句一起使用。

12.2 分組彙總

GROUP BY 子句可以將數據按照某種規則進行分組,並且爲每一個組返回一條記錄。在查詢語句中使用分組子句的語法如下:

SELECT col1,
       col2,
       aggregate_function(expression)
  FROM table_name
[WHERE conditions]
 GROUP BY col1, col2;

例如,以下查詢返回了不同部門中的員工數量和月薪總和:

select dept_id, count(*), sum(salary)
from employee
group by dept_id;
dept_id|count(*)|sum(salary)|
-------|--------|-----------|
      1|       3|   80000.00|
      2|       3|   41500.00|
      3|       2|   18000.00|
      4|       9|   68200.00|
      5|       8|   40100.00|

以下語句同時按照部門和性別統計員工的數量:

select dept_id, sex, count(*)
from employee
group by dept_id, sex;
dept_id|sex |count(*)|
-------|----|--------|
      1||       3|
      2||       3|
      3||       2|
      4||       8|
      4||       1|
      5||       8|

以下語句統計了每年入職的員工數量:

select extract(year from hire_date) as "入職年份",
       count(*) as "員工數量"
from employee
group by extract(year from hire_date);
入職年份|員工數量|
----|----|
2000|   3|
2006|   1|
2008|   1|
2007|   1|
2002|   2|
2005|   1|
2009|   1|
2011|   3|
2012|   2|
2010|   1|
2014|   1|
2017|   2|
2018|   5|
2019|   1|

GROUP BY 支持使用表達式進行分組。EXTRACT 函數用於提取日期中的年份信息,我們在後續文章中會介紹這個函數。

如果GROUP BY後的分組字段存在 NULL 值,多個 NULL 值將被看作一個分組。以下語句按照不同獎金值統計員工的數量:

SELECT bonus, COUNT(*)
  FROM employee
 GROUP BY bonus;

bonus   |COUNT(*)|
--------|--------|
10000.00|       3|
 8000.00|       1|
  [NULL]|      16|
 5000.00|       2|
 6000.00|       1|
 2000.00|       1|
 1500.00|       1|

從查詢結果可以看出,16 個員工沒有獎金;但是他們都被分組同一個組中,而不是多個不同的組。

在使用分組彙總時,初學者常見的一個錯誤就是在 SELECT 列表中使用了既不是聚合函數,也不屬於分組字段的字段。例如:

-- GROUP BY 錯誤示例
select dept_id, emp_name, avg(salary)
from employee
group by dept_id;
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'hrdb.employee.emp_name' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

以上語句返回了一個錯誤:字段 emp_name 沒有出現在 GROUP BY 子句或者聚合函數中。原因在於該查詢按照部門進行分組,但是每個部門包含多個員工;因此無法確定需要顯示哪個員工的姓名。

MySQL 通過 SQL 模式參數 ONLY_FULL_GROUP_BY 控制該行爲,默認值表示遵循 SQL 標準;如果禁用該參數,以上示例不會出錯。

另外,MySQL 也可以通過 ANY_VALUE 函數返回一個隨機的數據,可以避免以上錯誤:

select dept_id, any_value(emp_name), avg(salary)
from employee
group by dept_id;
dept_id|any_value(emp_name)|avg(salary) |
-------|-------------------|------------|
      1|劉備                |26666.666667|
      2|諸葛亮              |13833.333333|
      3|孫尚香              | 9000.000000|
      4|趙雲                | 7577.777778|
      5|法正                | 5012.500000|

需要小心的是,any_value 函數返回的數據是不確定的。

12.3 分組過濾

當我們需要對分組後的數據再次進行過濾,例如找出人數多於 5 個的部門時,如果在 WHERE 子句中增加一個過濾條件:

select dept_id, count(*)
from employee
where count(*) > 5
group by dept_id;
ERROR 1111 (HY000): Invalid use of group function

該語句執行出錯。錯誤的原因在於 WHERE 子句在 GROUP BY 子句之前執行,此時還沒有計算聚合函數,因此它只能基於分組之前的數據進行過濾。如果需要對分組後的結果進行過濾,需要使用HAVING子句。以上查詢的正確寫法如下:

select dept_id, count(*)
from employee
group by dept_id
having count(*) > 5;
dept_id|count(*)|
-------|--------|
      4|       9|
      5|       8|

HAVING 子句位於 GROUP BY 之後,並且必須與 GROUP BY 一起使用。

我們可以使用 WHERE 子句對錶進行過濾,同時使用 HAVING 對分組結果進行過濾。例如,以下語句返回了存在 2 名以上女性員工的部門:

select dept_id, count(*) cnt
from employee
where sex = '女'
group by dept_id
having cnt >= 2;
dept_id|cnt|
-------|---|
      3|  2|

首先通過 WHERE子句找出女性員工;然後,按照部門編號進行分組,計算每個組內的員工數量;最後,使用 HAVING 子句過濾員工數量等於或多於 2 個人的部門。MySQL 允許在 HAVING 子句中使用列的別名(cnt)進行過濾。

到目前爲止,我們學習過的完整查詢語句如下:

SELECT col1,
       col2,
       aggregate_function(expression)
  FROM table_name
 WHERE conditions
 GROUP BY col1, col2
HAVING conditions
 ORDER BY col1 [ASC | DESC], col2 [ASC | DESC], ...
 LIMIT [off_set,] row_count;

對於以上各個子句,MySQL 的邏輯執行順序爲 FROM、WHERE、SELECT、GROUP BY、HAVING、ORDER BY 以及 LIMIT。

12.4 高級分組

MySQL 中的 GROUP BY 子句還支持一個WITH ROLLUP選項,除了分組統計之外還會生成更高層級的彙總,類似於報表中的小計和總計。

首先創建一個銷售數據表:

CREATE TABLE sales (
    item VARCHAR(10),
    year VARCHAR(4),
    quantity INT
);

INSERT INTO sales VALUES('apple', '2018', 800);
INSERT INTO sales VALUES('apple', '2018', 1000);
INSERT INTO sales VALUES('banana', '2018', 500);
INSERT INTO sales VALUES('banana', '2018', 600);
INSERT INTO sales VALUES('apple', '2019', 1200);
INSERT INTO sales VALUES('banana', '2019', 1800);

使用以下查詢可以返回按照產品和年度統計的銷量小計,按照產品統計的銷量合計,以及所有產品的銷量總計:

select item, year, sum(quantity)
from sales
group by item, year with rollup;
item  |year|sum(quantity)|
------|----|-------------|
apple |2018|         1800|
apple |2019|         1200|
apple |    |         3000|
banana|2018|         1100|
banana|2019|         1800|
banana|    |         2900|
      |    |         5900|

其中,第三行數據表示 apple 在所有年度的銷量合計;最後一行表示所有產品在所有年度的銷量總計。

對於以下形式的 WITH ROLLUP 而言:

GROUP BY col1, col2 WITH ROLLUP

實際上等價於以下三種分組統計的結果相加:

GROUP BY col1, col2
GROUP BY col1
GROUP BY null

使用了 WITH ROLLUP 選項之後,會產生一些數據爲 NULL 的結果,表示相應字段上的彙總結果。但是這種顯示方式意義不明確,而且如果原數據也有 NULL 數據,則無法進行區分。因此 MySQL 提供了GROUPING()函數。

如果某個數據是彙總的小計或者總計,GROUPING() 函數返回 1;否則,返回 0。例如:

select item, year, sum(quantity), grouping(item), grouping(year), grouping(item, year)
from sales
group by item, year with rollup;
item  |year|sum(quantity)|grouping(item)|grouping(year)|grouping(item, year)|
------|----|-------------|--------------|--------------|--------------------|
apple |2018|         1800|             0|             0|                   0|
apple |2019|         1200|             0|             0|                   0|
apple |    |         3000|             0|             1|                   1|
banana|2018|         1100|             0|             0|                   0|
banana|2019|         1800|             0|             0|                   0|
banana|    |         2900|             0|             1|                   1|
      |    |         5900|             1|             1|                   3|

其中,第三行數據是按照年度計算的合計,grouping(item) 返回 0,grouping(year) 返回 1;最後一行是所有產品在所有年度的銷量總計,grouping(item) 返回 1,grouping(year) 返回 1。grouping(item, year) 的計算方式是 grouping(item) * 2 + grouping(year)。

對於 grouping(col1, col2, col3),計算的方式如下:

grouping(col1) * 4 + grouping(col2) * 2 + grouping(col3)

我們可以將上面的示例修改如下:

select if(grouping(item) = 1, '所有產品', item) as "產品", 
       if(grouping(year) = 1, '所有年度', item) as "年度", 
       sum(quantity) as "銷量"
from sales
group by item, year with rollup;
產品     |年度      |銷量 |
---------|---------|----|
apple    |apple    |1800|
apple    |apple    |1200|
apple    |所有年度  |3000|
banana   |banana   |1100|
banana   |banana   |1800|
banana   |所有年度  |2900|
所有產品   |所有年度  |5900|

其中,IF(expr1,expr2,expr3) 函數當 expr1 爲 TRUE 時(expr1 <> 0 and expr1 <> NULL)返回 expr2 的值;否則,返回 expr3 的值。

GROUPING() 函數可以用於 SELECT 列表、HAVING 子句以及 ORDER BY 子句中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章