五种主流数据库:分组统计

来源:SQL编程思想

如果将数据按照某种规则进行分组,然后分别进行汇总,通常能够得到更详细的分析结果。例如,按照不同性别计算员工的平均月薪,按照不同的产品和渠道统计销售金额等。为了实现这种分组统计的效果,我们可以将聚合函数与分组操作(GROUP BY)结合使用。

本文比较五种主流数据库实现的分组统计功能,包括 MySQL、Oracle、SQL Server、PostgreSQL 以及 SQLite。

功能MySQLOracleSQL ServerPostgreSQLSQLite
GROUP BY✔️✔️✔️✔️✔️
HAVING✔️✔️✔️✔️✔️

创建数据分组

GROUP BY 子句可以将数据按照某种规则进行分组。例如,以下查询使将员工按照性别进行分组:

SELECT sex AS "性别"
FROM employee
GROUP BY sex;

其中,GROUP BY 表示将性别的每个不同取值分为一组,每个组返回一条记录。查询返回的结果如下:

性别
---


员工表中只存在 2 种不同的性别,因此返回了 2 条记录。我们也可以通过 DISTINCT 运算符实现相同的结果:

SELECT DISTINCT sex AS "性别"
FROM employee;

其中,DISTINCT 表示返回不重复的数据,查询结果和上面的示例相同。

我们也可以基于多个字段或表达式进行分组,从而创建更详细的分组。例如,以下语句按照不同的部门和性别进行分组:

SELECT dept_id AS "部门编号", sex AS "性别"
FROM employee
GROUP BY dept_id, sex;

查询返回的结果如下:

部门编号|性别
------|---
    1|
    2|
    3|
    4|
    4|
    5|

研发部(部门编号为 4)既有男性员工,又有女性员工,因此分为 2 个组。

进行组内汇总

我们可以结合使用 GROUP BY 子句与聚合函数,将数据进行分组,并在每个组内进行一次数据汇总。例如,以下语句按照不同的性别统计员工数量和平均月薪:

SELECT sex AS "性别", 
      COUNT(*) AS "员工数量",
      AVG(salary) AS "平均月薪"
FROM employee
GROUP BY sex;

其中 GROUP BY 用于将员工按照性别分为男、女两个组,然后利用 COUNT 和 AVG 函数分别计算男性员工和女性员工的总数和平均月薪。查询返回的结果如下:

性别|员工数量|平均月薪 
---|-------|------------
|     22|10054.545455
|      3| 8200.000000

男性员工有 22 人,平均月薪约为 10055 元;女性员工有 3 人,平均月薪为 8200 元。以下查询统计了每年入职的员工数量:

-- Oracle、MySQL 以及 PostgreSQL
SELECT EXTRACT(YEAR FROM hire_date) AS "入职年份",
COUNT(*) AS "员工数量"
FROM employee
GROUP BY EXTRACT(YEAR FROM hire_date)
ORDER BY COUNT(*) DESC;

-- Microsoft SQL Server
SELECT DATEPART(YEAR, hire_date) AS "入职年份",
COUNT(*) AS "员工数量"
FROM employee
GROUP BY DATEPART(YEAR, hire_date)
ORDER BY COUNT(*) DESC;

-- SQLite
SELECT STRFTIME('%Y', hire_date) AS "入职年份",
COUNT(*) AS "员工数量"
FROM employee
GROUP BY STRFTIME('%Y', hire_date)
ORDER BY COUNT(*) DESC;

我们在以上查询中使用了基于表达式的分组汇总,其中 EXTRACT、DATEPART 以及 STRFTIME 是不同数据库中用于提取日期信息的函数。查询返回的结果如下:

入职年份|员工数量
-------|-------
  2018| 5
  2000| 3
  2011| 3
  2002| 2
  2012| 2
  2017| 2
...

我们在使用 GROUP BY 子句进行分组时,如果分组字段中存在多个空值(NULL),它们将被分为一个组,而不是多个组。例如,以下查询按照不同奖金额统计员工的数量:

SELECT bonus AS "奖金", COUNT(*) AS "员工数量"
FROM employee
GROUP BY bonus;

查询返回的结果如下:

奖金    |员工数量
--------|-------
10000.00| 3
8000.00| 1
       | 16
5000.00| 2
6000.00| 1
2000.00| 1
1500.00| 1

查询结果显示 16 位员工没有奖金,他们都被分到了同一个组。

提示:虽然 SQL 中的 NULL 和 NULL 不同,但是 GROUP BY 子句会将多个 NULL 值分为一组,也就是执行分组操作的时候认为它们相等。

常见语法问题

初学者在使用分组汇总操作时经常会犯的一个错误就是,在 SELECT 列表中使用了 GROUP BY 子句之外的字段。例如:

-- GROUP BY 错误示例
SELECT dept_id, emp_name, AVG(salary)
FROM employee
GROUP BY dept_id;

以上语句在大多数数据库中都会返回一个类似的错误:emp_name 字段没有出现在 GROUP BY 子句或者聚合函数中。

这个错误的原因在于,我们想要按照部门进行分组,但是每个部门包含多名员工,数据库无法确定显示哪个员工的姓名。这是一个逻辑上的错误,而不是数据库实现的问题。

注意:MySQL 通过 sql_mode 参数 ONLY_FULL_GROUP_BY 控制该行为,默认遵循 SQL 标准;但是如果禁用该参数,以上示例将不会报错,而是随机返回一个员工姓名。以上示例在 SQLite 中也不会报错,而是随机返回一个员工姓名。

再次过滤数据

我们通常使用 WHERE 子句进行数据过滤,但是如果需要对分组汇总的结果进行过滤,是不是也可以使用 WHERE 子句实现呢?以下语句统计了每个部门的平均月薪,然后返回平均月薪大于 10 000 元的部门:

-- 使用 WHERE 子句进行数据过滤的错误示例
SELECT dept_id, AVG(salary)
FROM employee
WHERE AVG(salary) > 10000
GROUP BY dept_id;

以上语句在 5 种数据库中都返回了类似的错误信息:WHERE 子句中不允许使用聚合函数。

这个错误的原因在于,WHERE 子句会针对 FROM 子句中的数据行进行过滤,在 WHERE子句执行时还没有进行分组汇总操作,还没有计算出 AVG(salary)函数的值,因此不允许使用聚合函数。

为了对分组汇总后的数据再次进行过滤,SQL 提供了另一个过滤数据的子句:HAVING。我们可以使用 HAVING 子句将上面的错误示例修改如下:

SELECT dept_id AS "部门编号", AVG(salary) AS "平均月薪"
FROM employee
GROUP BY dept_id
HAVING AVG(salary) > 10000;

其中,HAVING 子句必须与 GROUP BY 子句一起使用,并且位于 GROUP BY 子句之后,表示对 AVG(salary)函数的结果进行过滤。查询返回的结果如下:

部门编号|平均月薪 
------|------------
    1|26666.666667
    2|13166.666667

我们可以使用 WHERE 子句对表进行数据过滤,同时使用 HAVING 子句对分组结果进行过滤。例如,以下语句查询拥有 2 名以上女性员工的部门:

SELECT dept_id AS "部门编号", COUNT(*) AS "员工数量"
FROM employee
WHERE sex = '女'
GROUP BY dept_id
HAVING COUNT(*) >= 2;

其中,WHERE 子句用于检索女性员工,GROUP BY 子句按照部门统计女性员工的数量,HAVING 子句选择数量大于或等于 2 的部门。查询返回的结果如下:

部门编号|员工数量
------|-------
3| 2

只有财务部(dept_id=3)中有 2 名女性员工。

提示:从性能的角度来说,我们应该尽量使用 WHERE 子句过滤掉更多的数据,而不是等到分组之后再通过 HAVING 子句进行过滤。但是如果业务需求只能基于汇总之后的结果进行过滤,那就另当别论了。

请使用浏览器的分享功能分享到微信等