【MySQL】数据分析双剑客:聚合函数 与 group by子句的完美搭配

文章目录

  • [1. 聚合函数](#1. 聚合函数)
    • [1.1 count](#1.1 count)
      • [1.1.1 统计班级同学总数](#1.1.1 统计班级同学总数)
      • [1.1.2 统计非空数学成绩的数量](#1.1.2 统计非空数学成绩的数量)
      • [1.1.3 统计去重后的语文成绩个数](#1.1.3 统计去重后的语文成绩个数)
    • [1.2 sum](#1.2 sum)
      • [1.2.1 统计全班同学数学总分](#1.2.1 统计全班同学数学总分)
      • [1.2.3 统计全班同学数学成绩去重后总分](#1.2.3 统计全班同学数学成绩去重后总分)
      • [1.2.4 统计全班同学数学成绩*0.88后的总分](#1.2.4 统计全班同学数学成绩*0.88后的总分)
    • [1.3 avg](#1.3 avg)
      • [1.3.1 计算班级数学平均分](#1.3.1 计算班级数学平均分)
      • [1.3.2 计算班级总成绩平均分](#1.3.2 计算班级总成绩平均分)
    • [1.4 max](#1.4 max)
    • [1.5 min](#1.5 min)
  • [2. group by子句](#2. group by子句)
    • [2.1 为什么需要GROUP BY?](#2.1 为什么需要GROUP BY?)
    • [2.2 语法](#2.2 语法)
    • [2.3 案例](#2.3 案例)
      • [2.3.1 准备工作](#2.3.1 准备工作)
      • [2.3.2 显示每个部门的平均工资和最高工资](#2.3.2 显示每个部门的平均工资和最高工资)
      • [2.3.3 如何理解"聚合"二字?](#2.3.3 如何理解“聚合”二字?)
      • [2.3.4 显示每个部门的每种岗位的平均工资和最高工资](#2.3.4 显示每个部门的每种岗位的平均工资和最高工资)
      • [2.3.5 使用group by子句的重要原则*](#2.3.5 使用group by子句的重要原则*)
      • [2.3.6 平均工资低于2000的部门和它的平均工资(having的使用)*](#2.3.6 平均工资低于2000的部门和它的平均工资(having的使用)*)
      • [2.3.7 平均工资低于2000的部门和它的平均工资 且员工SMITH不参与统计](#2.3.7 平均工资低于2000的部门和它的平均工资 且员工SMITH不参与统计)
  • [3. 执行顺序的理解+MySQL中having可以使用别名](#3. 执行顺序的理解+MySQL中having可以使用别名)

1. 聚合函数

下面通过案例来熟悉这些函数得使用,使用的表依然是exam_result

1.1 count

在SQL数据分析中,一个最基本且最常见的问题就是:"有多少?"

网站今天有多少访客?

库存中有多少种产品?

本月完成了多少笔订单?

回答这些问题的核心工具就是 COUNT() 函数。

1.1.1 统计班级同学总数

统计总数,那使用的就是count

那这里要统计同学的总人数

怎么做呢?
select count(*) from exam_result;
这就统计了表中所有记录的总数

大家可以数一下,就是9个同学(id有缺失)
当然,也可以重命名

或者:
select count(name) from exam_result;统计name这一列的行数,有多少name不就有多少个同学嘛。

甚至还可以这样

结果都是正确的

怎么回事呢?解释一下

select count(*) from 表名;
统计表中的总行数(包括NULL值)
select * from exam_result;就是显示表中所有的记录嘛,把*放到count()中就是统计表中所有记录的数量(包括null)。
count()里面放对应的列名,就是统计该列有多少行数据。
但注意:COUNT(列名) - 只统计特定列的非NULL值数量,所以结果并不总和COUNT(*)结果相同。
select count(1) from exam_result;又是怎么回事?
COUNT(常数) ,使用任意常量做参数,效果与COUNT(*)相同
select * from exam_result;是显示表中所有记录
如果select 常数 from 表名;

就会显示与表中记录等数量的该常数

1.1.2 统计非空数学成绩的数量

假设本次考试中,数学有些同学缺考了,最后录入的成绩为null,现在我想统计一下考数学的同学数量(即数学成绩非空的数量)


首先看到,一个参加考试的同学11个人,数学为空(即缺考的2人),所以结果是9
上面我们提到COUNT(列名) - 只统计特定列的非NULL值数量 ,所以结果并不总和COUNT(*)结果相同
我们来试一下

没问题,答案是9,没用统计null的记录

count(*)统计则包含null值

1.1.3 统计去重后的语文成绩个数

上面的用法中如果记录用重复值,也会重复计数

现在我看到语文成绩中有些成绩一样,我想统计去重后的数量,怎么做?


select count(chinese) from exam_result;这统计了所有语文成绩的数量。
要统计去重后的
select distinct count(chinese) from exam_result;这样写?
对吗?验证一下

还是11,这种写法是错的。
这样写是对count(chinese)去重,count(chinese)的结果就一个数字,去什么重啊!
我们要对语文成绩去重,所以:
select count(distinct chinese) from exam_result;
这样写才对。统计去重后的语文成绩的个数。

1.2 sum

在数据分析中,"总和" 是最基本也是最重要的统计指标之一:

公司年收入是多少?

项目总成本是多少?

用户总消费额是多少?

SUM()函数就是专门为回答这些问题而生的。

1.2.1 统计全班同学数学总分

那就用到sum函数:

select sum(math) from exam_result;

当然使用这些函数的时候后面也可以跟条件:

比如,统计数学成绩<90分的同学的数学总成绩
select sum(math) from exam_result where math<90;

1.2.3 统计全班同学数学成绩去重后总分

同样也可以去重,用法和上面一样

1.2.4 统计全班同学数学成绩*0.88后的总分

对表达式求和:


1.3 avg

在数据分析中,平均值是衡量集中趋势的关键指标:

员工的平均工资是多少?

订单的平均金额是多少?

用户平均每天使用应用多长时间?

AVG()函数正是用来计算这些平均值的。

1.3.1 计算班级数学平均分

两种方法:

我们可以自己列式子算
select sum(math)/count(*) from exam_result;

但是注意,count(*)统计个数把null值也会计入。
当然这里计算平均分你带不带缺考的人好像都合理。
如果严格计算考试数学的所有同学平均数学成绩,那么

就应该这样
第二种方法,使用avg函数计算
select avg(math) from exam_result;

能看到两种方法的结果是一样的。
当然,也可以加条件或计算表达式的平均值或者加条件啥的。

比如,计算一些全班同学总成绩的平均分

1.3.2 计算班级总成绩平均分

select avg(math+chinese+english) from exam_result;

1.4 max

在数据世界中,寻找"最大值"是一种基本而强大的分析方式:

谁是销售额最高的员工?

哪一天是网站的流量高峰?

产品的最高温度是多少?

哪条记录的创建时间最近?

MAX()函数就是专门为寻找这些"峰值"而设计的工具。

比如:找出班级同学中英语成绩的最高分

select max(english) from exam_result;

1.5 min

数据分析中,我们不仅关注"峰值",同样关注"谷值":

什么是最低价格?用于采购决策

最早发生时间是什么时候?用于历史分析

最差的性能指标是多少?用于质量监控

最短的处理时间是多少?用于效率优化

MIN()函数是发现数据"底线"的关键工具,帮助我们理解数据的完整范围。

比如:返回 > 70 分以上的数学最低分:

select min(math) from exam_result where math>70;

2. group by子句

2.1 为什么需要GROUP BY?

在数据库中,我们经常需要对数据进行分类汇总,例如:

统计每个部门的员工数量

计算每个产品的销售总额

找出每个班级的平均分数

你不可能手动逐条统计。这就是GROUP BY大显身手的时候!它让SQL从行级处理跃升到组级处理,是数据分析的基石。

2.2 语法

语法:

sql 复制代码
select column1, column2, .. from table group by column;

简单看一下,下面通过练习大家就懂了

2.3 案例

2.3.1 准备工作

准备工作,来自oracle 9i的经典测试表,包括:

EMP员工表

DEPT部门表

SALGRADE工资等级表

下面是建表的SQL语句:

sql 复制代码
DROP database IF EXISTS `scott`;
CREATE database IF NOT EXISTS `scott` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

USE `scott`;

DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
  `deptno` int(2) unsigned zerofill NOT NULL COMMENT '部门编号',
  `dname` varchar(14) DEFAULT NULL COMMENT '部门名称',
  `loc` varchar(13) DEFAULT NULL COMMENT '部门所在地点'
);


DROP TABLE IF EXISTS `emp`;
CREATE TABLE `emp` (
  `empno` int(6) unsigned zerofill NOT NULL COMMENT '雇员编号',
  `ename` varchar(10) DEFAULT NULL COMMENT '雇员姓名',
  `job` varchar(9) DEFAULT NULL COMMENT '雇员职位',
  `mgr` int(4) unsigned zerofill DEFAULT NULL COMMENT '雇员领导编号',
  `hiredate` datetime DEFAULT NULL COMMENT '雇佣时间',
  `sal` decimal(7,2) DEFAULT NULL COMMENT '工资月薪',
  `comm` decimal(7,2) DEFAULT NULL COMMENT '奖金',
  `deptno` int(2) unsigned zerofill DEFAULT NULL COMMENT '部门编号'
);


DROP TABLE IF EXISTS `salgrade`;
CREATE TABLE `salgrade` (
  `grade` int(11) DEFAULT NULL COMMENT '等级',
  `losal` int(11) DEFAULT NULL COMMENT '此等级最低工资',
  `hisal` int(11) DEFAULT NULL COMMENT '此等级最高工资'
);

大家先自己熟悉一下这几张表,里面comment都标识了每个字段的含义

那么我已经将这个数据库导入到了我的MySQL中并插入了一些数据:


表结构:

表内容

预备工作全部完成,下面利用这几张表来讲解group by子句

2.3.2 显示每个部门的平均工资和最高工资

简单分析一下:

其实学了上面的聚合函数,如果是统计全公司的平均工资和最高工资,那么非常简单了


但是,现在要统计每个部门的平均工资和最高工资。
那就想到按部门(这里就是用部门编号deptno)对他们进行分组,然后统计每个分组的平均工资和最高工资。

所以,group by就可以上场了

select deptno,avg(sal) 平均工资 ,max(sal) 最高工资 from emp group by deptno;

根据部门编号去分组,由于有多个不同的部门(对应多个不同的部门编号),所以最终就分成了多个不同的组(可以理解成把一张表逻辑上 拆分成了多张子表,在这些子表内,我们就可以直接统计它的诸如最大值,平均值这些聚合的统计,不就变成上面我们一开始学的内容了)。
而在一组内的数据,它们的部门编号一定是一样的,那么就可以对他们使用聚合函数,通过聚合,可以将大量数据压缩成更少、更简洁的汇总数据,便于人类理解和分析。

2.3.3 如何理解"聚合"二字?

上面讲解聚合函数的时候并没有带大家理解"聚合"的含义:

假设有一个销售表,包含每个员工的销售额。如果我们想知道所有员工的总销售额,可以使用SUM()函数,这就是一个聚合操作,将多个员工的销售额聚合成一个总销售额。

因此,"聚合"二字非常贴切地描述了这些函数的功能:将多个值聚合成一个值。

"聚合"在SQL中不仅仅是一种函数类型,更是一种数据思维方式。掌握了聚合思维,你就掌握了从海量数据中提取洞察的关键能力。每一次使用SUM()、AVG()、MAX(),你都是在进行数据的抽象和升华,将原始数据的"原材料"加工成有意义的"信息产品"。

2.3.4 显示每个部门的每种岗位的平均工资和最高工资

刚才我们求的是每个部门的平均工资和最高工资,现在要求每个部门的每种岗位

那么上面我们是按部门分组,所以现在就要先按部门分组,然后每个部门再按岗位分组(一个部门中有多个不同的岗位)
那要怎么写呢?
先对部门分组,就是我们上面写的,然后在对岗位分组,直接,后面跟岗位的字段名即可
select deptno,job,avg(sal) 平均工资,max(sal) 最高工资 from emp group by deptno,job;

我们发现这比上次的记录条数变多了,因为我们分组的数量也更多了。

然后我做这样一件事情:

select ename,deptno,job,avg(sal) 平均工资,max(sal) 最高工资 from emp group by deptno,job;
select子句中我把员工名字ename字段也加上

但是发现报错了,报错说了什么呢?

原因在于:
这样做其实存在一个逻辑错误,我们先按部门分组,然后一个部门中又按照岗位分组,最终分出来的这个组中,它们的所属部门一定是一样的,并且岗位是一样的。所以我们可以对它进行"聚合",求得该组中的最高工资和平均工资。
但是现在把员工名字加上,就有一个问题:同一个岗位可能有多名员工,它们的名字不可能全部相同,所以
这里MySQL不知道应该显示组里的哪个员工的名字。

2.3.5 使用group by子句的重要原则*

记住这个原则:

GROUP BY之后,每个分组在结果中只对应一条记录。
使用group by子句的场景下,select后面跟的列,要么在GROUP BY子句中出现,要么被聚合函数处理 ,因为被聚合函数处理后,每个分组中该列对应的多行可能不同的值通过聚合函数计算得到一个单一的值,从而也保证了每个分组只返回一行

而刚才我们加的ename字段并不属于这两种情况之一,所以报错了。

通过理解这个原则,你就能写出正确且高效的GROUP BY查询,而不仅仅是规避错误。

2.3.6 平均工资低于2000的部门和它的平均工资(having的使用)*

这个问题我们分两步来做:

先统计出各个部门的平均工资,然后再对这个结果进行过滤,筛选出我们要的结果
第一步,统计出各个部门的平均工资
select deptno 部门号,avg(sal) 平均工资 from emp group by deptno;

这是所有部门的平均工资,但是现在我们只要平均工资低于2000的。
怎么做?加一个where子句?

但是报错了,直接语法错误。

🆗,我们使用了GROUP BY进行分组,然后想要筛选分组后的结果,但是不能使用WHERE子句,因为WHERE是对原始数据(任意列)进行筛选,而不能过滤分组后的聚合结果。
分组后的筛选应该使用HAVING
select deptno 部门号,avg(sal) 平均工资 from emp group by deptno having avg(sal)<2000;

这样就可以了。
另外对于一张没用被分组过的表,你如果条件查询时候用了having其实会发现是可以的(当然不推荐这样做)

因为一张表没被分组过,但是也可以被看成是一组啊(所以上面我们说了分组逻辑上可以理解成为划分子表)。

2.3.7 平均工资低于2000的部门和它的平均工资 且员工SMITH不参与统计

怎么做?

select deptno 部门号,avg(sal) 平均工资 from emp group by deptno having avg(sal)<2000;
这是上一问的语句,那我们在having后面再加一个条件ename!='SMITH';

但是报错了,为什么呢?
同样地,我们说了having是对分组之后的结果进行筛选,分组后一组中可能有多个不同ename(员工姓名),这时MySQL就不知道应该使用哪个ename来判断 != 'SMITH'了。
所以,在HAVING子句中,也只能使用在GROUP BY子句中出现的列或聚合函数处理的列

那正确的应该怎么改?

将条件!= 'SMITH'移到WHERE子句。
先把SMITH这个员工的记录从表中移除

即对这张不包含SMITH这个员工的记录的表进行操作
select deptno 部门号,avg(sal) 平均工资 from emp where ename!='SMITH' group by deptno having avg(sal)<2000;

可以看到结果和上一题一样,因为SMITH这个人不在平均工资低于2000的部门。

3. 执行顺序的理解+MySQL中having可以使用别名

目前我们学的这些SQL子句,它们的执行顺序应该是怎么样的:

那现在就有一个问题需要注意一下:

上面的这个例子中

如果我们在having子句中使用平均工资这个别名(前面我们讲了where子句和order by子句中的别名使用问题)

其实是可以的。
但是按照上面给的这个顺序

按照标准,HAVING在SELECT之前执行,所以理论上不能使用SELECT中的别名。 因为别名我们是在select子句中定义的。
那这里为啥可以呢?
可以这样理解(这里问了deepseek):
从 SQL 标准以及执行顺序来看,HAVING 子句是在 SELECT 子句之前执行的,因此从逻辑上讲,HAVING 子句中不能使用 SELECT 子句中定义的别名。
但是,MySQL 对 SQL 进行了扩展,允许在 HAVING 子句中使用 SELECT 子句中的别名。这是因为 MySQL 在执行时可能会进行查询重写,或者说是为了方便。

相关推荐
TracyCoder1232 小时前
全面解析:Elasticsearch 性能优化指南
大数据·elasticsearch·性能优化
Dxy12393102162 小时前
Python判断MySQL表是否存在,不存在则创建
python·mysql·adb
我是黄骨鱼2 小时前
【零基础学数据库|第一篇】绪论
mysql
BYSJMG2 小时前
2026计算机毕设推荐:基于大数据的车辆二氧化碳排放量可视化分析系统
大数据·vue.js·python·mysql·django·课程设计
白日梦想家6812 小时前
JavaScript性能优化实战系列(三篇完整版)
开发语言·javascript·性能优化
橘子132 小时前
MySQL连接(十四)
数据库·mysql
渡我白衣3 小时前
【MySQL基础】(2):数据库基础概念
数据库·人工智能·深度学习·神经网络·mysql·机器学习·自然语言处理
怣503 小时前
MySQL WHERE子句完全指南:精准过滤数据的艺术
数据库·mysql
王和阳3 小时前
一种简洁优雅的纯客户端 Snapshot 方案
性能优化·indexeddb·snapshot