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 子句中。