|
|
|
@ -18,10 +18,223 @@
|
|
|
|
|
-
|
|
|
|
|
## 1. 实践
|
|
|
|
|
|
|
|
|
|
### 1.8 聚合函数:怎么高效地进行分组统计?
|
|
|
|
|
- MySQL 中有 5 种聚合函数较为常用,分别是求和函数 SUM()、求平均函数 AVG()、最大值函数 MAX()、最小值函数 MIN() 和计数函数 COUNT()
|
|
|
|
|
- 项目需求是这样的:超市经营者提出,他们需要统计某个门店,每天、每个单品的销售情况,包括销售数量和销售金额等。这里涉及 3 个数据表,具体信息如下所示:
|
|
|
|
|
- 销售明细表(demo.transactiondetails):
|
|
|
|
|
- ![聚合函数销售明细表](pic/聚合函数销售明细表.png)
|
|
|
|
|
- 销售单头表(demo.transactionhead):
|
|
|
|
|
- ![销售单头表](pic/聚合函数销售单头表.png)
|
|
|
|
|
- 商品信息表(demo.goodsmaster):
|
|
|
|
|
- ![聚合函数商品信息表](pic/聚合函数商品信息表.png)
|
|
|
|
|
|
|
|
|
|
- SUM()
|
|
|
|
|
- SUM()函数可以返回指定字段值的和。我们可以用它来获得用户某个门店,每天,每种商品的销售总计数据:
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT
|
|
|
|
|
-> LEFT(b.transdate, 10), -- 从关联表获取交易时间,并且通过LEFT函数,获取交易时
|
|
|
|
|
-> c.goodsname, -- 从关联表获取商品名称
|
|
|
|
|
-> SUM(a.quantity), -- 数量求和
|
|
|
|
|
-> SUM(a.salesvalue) -- 金额求和
|
|
|
|
|
-> FROM
|
|
|
|
|
-> demo.transactiondetails a
|
|
|
|
|
-> JOIN
|
|
|
|
|
-> demo.transactionhead b ON (a.transactionid = b.transactionid)
|
|
|
|
|
-> JOIN
|
|
|
|
|
-> demo.goodsmaster c ON (a.itemnumber = c.itemnumber)
|
|
|
|
|
-> GROUP BY LEFT(b.transdate, 10) , c.goodsname -- 分组
|
|
|
|
|
-> ORDER BY LEFT(b.transdate, 10) , c.goodsname; -- 排序
|
|
|
|
|
+-----------------------+-----------+-----------------+-------------------+
|
|
|
|
|
| LEFT(b.transdate, 10) | goodsname | SUM(a.quantity) | SUM(a.salesvalue) |
|
|
|
|
|
+-----------------------+-----------+-----------------+-------------------+
|
|
|
|
|
| 2020-12-01 | 书 | 2.000 | 178.00 |
|
|
|
|
|
| 2020-12-01 | 笔 | 5.000 | 25.00 |
|
|
|
|
|
| 2020-12-02 | 书 | 4.000 | 356.00 |
|
|
|
|
|
| 2020-12-02 | 笔 | 16.000 | 80.00 |
|
|
|
|
|
+-----------------------+-----------+-----------------+-------------------+
|
|
|
|
|
4 rows in set (0.01 sec)
|
|
|
|
|
```
|
|
|
|
|
- 我们引入了 2 个关键字:LEFT 和 ORDER BY
|
|
|
|
|
- LEFT(str,n):**表示返回字符串 str 最左边的 n 个字符**。我们这里的 LEFT(a.transdate,10),表示返回交易时间字符串最左边的 10 个字符。在 MySQL 中,
|
|
|
|
|
DATETIME 类型的默认格式是:YYYY-MM-DD,也就是说,年份 4 个字符,之后是“-”,然后是月份 2 个字符,之后又是“-”,然后是日 2 个字符,所以完整的年月日
|
|
|
|
|
是 10 个字符。用户要求按照日期统计,所以,**我们需要从日期时间数据中,把年月日的部分截取出来**。
|
|
|
|
|
- ORDER BY:**表示按照指定的字段排序**。超市经营者指定按照日期和单品统计,那么,统计的结果按照交易日期和商品名称的顺序排序,会更加清晰。
|
|
|
|
|
- 上述步骤是分为3步
|
|
|
|
|
- 第一步,完成 3 个表的连接
|
|
|
|
|
- 第二步,对结果集按照交易时间和商品名称进行分组
|
|
|
|
|
- 第三步,对各组的销售数量和销售金额进行统计,并且按照交易日期和商品名称排序
|
|
|
|
|
- **如果用户需要知道全部商品销售的总计数量和总计金额,我们也可以把数据集的整体看作一个分组,进行计算**。这样就不需要分组关键字 GROUP BY,以及排序关键字 ORDER BY
|
|
|
|
|
了。你甚至不需要从关联表中获取数据,也就不需要连接了。就像下面这样:
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT
|
|
|
|
|
-> SUM(quantity), -- 总计数量
|
|
|
|
|
-> SUM(salesvalue)-- 总计金额
|
|
|
|
|
-> FROM
|
|
|
|
|
-> demo.transactiondetails;
|
|
|
|
|
+---------------+-----------------+
|
|
|
|
|
| SUM(quantity) | SUM(salesvalue) |
|
|
|
|
|
+---------------+-----------------+
|
|
|
|
|
| 27.000 | 639.00 |
|
|
|
|
|
+---------------+-----------------+
|
|
|
|
|
1 row in set (0.05 sec)
|
|
|
|
|
```
|
|
|
|
|
- **AVG()、MAX()和 MIN()**
|
|
|
|
|
- 举个例子,如果用户需要计算每天、每种商品,平均一次卖出多少个、多少钱,这个时候,我们就可以用到 AVG()函数了
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT
|
|
|
|
|
-> LEFT(a.transdate, 10),
|
|
|
|
|
-> c.goodsname,
|
|
|
|
|
-> AVG(b.quantity), -- 平均数量
|
|
|
|
|
-> AVG(b.salesvalue) -- 平均金额
|
|
|
|
|
-> FROM
|
|
|
|
|
-> demo.transactionhead a
|
|
|
|
|
-> JOIN
|
|
|
|
|
-> demo.transactiondetails b ON (a.transactionid = b.transactionid)
|
|
|
|
|
-> JOIN
|
|
|
|
|
-> demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
|
|
|
|
|
-> GROUP BY LEFT(a.transdate,10),c.goodsname
|
|
|
|
|
-> ORDER BY LEFT(a.transdate,10),c.goodsname;
|
|
|
|
|
+-----------------------+-----------+-----------------+-------------------+
|
|
|
|
|
| LEFT(a.transdate, 10) | goodsname | AVG(b.quantity) | AVG(b.salesvalue) |
|
|
|
|
|
+-----------------------+-----------+-----------------+-------------------+
|
|
|
|
|
| 2020-12-01 | 书 | 2.0000000 | 178.000000 |
|
|
|
|
|
| 2020-12-01 | 笔 | 5.0000000 | 25.000000 |
|
|
|
|
|
| 2020-12-02 | 书 | 2.0000000 | 178.000000 |
|
|
|
|
|
| 2020-12-02 | 笔 | 8.0000000 | 40.000000 |
|
|
|
|
|
+-----------------------+-----------+-----------------+-------------------+
|
|
|
|
|
4 rows in set (0.00 sec)
|
|
|
|
|
```
|
|
|
|
|
- **MAX()和 MIN()**
|
|
|
|
|
- MAX() 表示获取指定字段在分组中的最大值,MIN() 表示获取指定字段在分组中的最小值。它们的实现原理差不多,下面我就重点讲一下 MAX(),知道了它的用法,MIN() 也就
|
|
|
|
|
很好理解了。
|
|
|
|
|
- 假如用户要求计算每天里的一次销售的最大数量和最大金额,就可以用下面的代码,得到我们需要的结果:
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT
|
|
|
|
|
-> LEFT(a.transdate, 10),
|
|
|
|
|
-> MAX(b.quantity), -- 数量最大值
|
|
|
|
|
-> MAX(b.salesvalue) -- 金额最大值
|
|
|
|
|
-> FROM
|
|
|
|
|
-> demo.transactionhead a
|
|
|
|
|
-> JOIN
|
|
|
|
|
-> demo.transactiondetails b ON (a.transactionid = b.transactionid)
|
|
|
|
|
-> JOIN
|
|
|
|
|
-> demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
|
|
|
|
|
-> GROUP BY LEFT(a.transdate,10)
|
|
|
|
|
-> ORDER BY LEFT(a.transdate,10);
|
|
|
|
|
+-----------------------+-----------------+-------------------+
|
|
|
|
|
| LEFT(a.transdate, 10) | MAX(b.quantity) | MAX(b.salesvalue) |
|
|
|
|
|
+-----------------------+-----------------+-------------------+
|
|
|
|
|
| 2020-12-01 | 5.000 | 178.00 |
|
|
|
|
|
| 2020-12-02 | 10.000 | 267.00 |
|
|
|
|
|
+-----------------------+-----------------+-------------------+
|
|
|
|
|
2 rows in set (0.00 sec)
|
|
|
|
|
```
|
|
|
|
|
- 千万不要以为 MAX(b.quantity)和 MAX(b.salesvalue)算出的结果一定是同一条记录的数据。实际上,**MySQL 是分别计算的**。下面我们就来分析一下刚刚的查询。
|
|
|
|
|
- MAX(字段)这个函数返回分组集中最大的那个值。如果你要查询 MAX(字段1)和 MAX(字段 2),而它们是相互独立、分别计算的,你千万不要想当然地认为结果
|
|
|
|
|
在同一条记录上。
|
|
|
|
|
|
|
|
|
|
- **COUNT()**
|
|
|
|
|
- 怎么解决卡顿的问题呢?我们想到了一个分页的策略。
|
|
|
|
|
- 所谓的分页策略,其实就是,不把查询的结果一次性全部返回给客户端,而是根据用户电脑屏幕的大小,计算一屏可以显示的记录数,每次只返回用户电脑屏幕可以显示的数据
|
|
|
|
|
集。接着,再通过翻页、跳转等功能按钮,实现查询目标的精准锁定。这样一来,每次查询的数据量较少,也就大大提高了系统响应速度。
|
|
|
|
|
- 这个策略能够实现的一个关键,就是要**计算出符合条件的记录一共有多少条**,之后才能计算出一共有几页、能不能翻页或跳转。
|
|
|
|
|
- 要计算记录数,就要用到 COUNT() 函数了。这个函数有两种情况。
|
|
|
|
|
- COUNT(*):统计一共有多少条记录;
|
|
|
|
|
- COUNT(字段):统计有多少个不为空的字段值。
|
|
|
|
|
|
|
|
|
|
- 如果 COUNT(*)与 GROUP BY 一起使用,就表示统计分组内有多少条数据。它也可以单独使用,这就相当于数据集全体是一个分组,统计全部数据集的记录数。
|
|
|
|
|
- 假设我有个销售流水明细表如下:
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT *
|
|
|
|
|
-> FROM demo.transactiondetails;
|
|
|
|
|
+---------------+------------+----------+-------+------------+
|
|
|
|
|
| transactionid | itemnumber | quantity | price | salesvalue |
|
|
|
|
|
+---------------+------------+----------+-------+------------+
|
|
|
|
|
| 1 | 1 | 2.000 | 89.00 | 178.00 |
|
|
|
|
|
| 1 | 2 | 5.000 | 5.00 | 25.00 |
|
|
|
|
|
| 2 | 1 | 3.000 | 89.00 | 267.00 |
|
|
|
|
|
| 2 | 2 | 6.000 | 5.00 | 30.00 |
|
|
|
|
|
| 3 | 1 | 1.000 | 89.00 | 89.00 |
|
|
|
|
|
| 3 | 2 | 10.000 | 5.00 | 50.00 |
|
|
|
|
|
+---------------+------------+----------+-------+------------+
|
|
|
|
|
6 rows in set (0.00 sec)
|
|
|
|
|
```
|
|
|
|
|
- 如果我们一屏可以显示 30 行,需要多少页才能显示完这个表的全部数据呢?
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT COUNT(*)
|
|
|
|
|
-> FROM demo.transactiondetails;
|
|
|
|
|
+----------+
|
|
|
|
|
| COUNT(*) |
|
|
|
|
|
+----------+
|
|
|
|
|
| 6 |
|
|
|
|
|
+----------+
|
|
|
|
|
1 row in set (0.03 sec)
|
|
|
|
|
```
|
|
|
|
|
- 我们这里只有 6 条数据,一屏就可以显示了,所以一共 1 页。
|
|
|
|
|
- 那么,如果超市经营者想知道,每天、每种商品都有几次销售,我们就需要按天、按商品名称,进行分组查询:
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT
|
|
|
|
|
-> LEFT(a.transdate, 10), c.goodsname, COUNT(*) -- 统计销售次数
|
|
|
|
|
-> FROM
|
|
|
|
|
-> demo.transactionhead a
|
|
|
|
|
-> JOIN
|
|
|
|
|
-> demo.transactiondetails b ON (a.transactionid = b.transactionid)
|
|
|
|
|
-> JOIN
|
|
|
|
|
-> demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
|
|
|
|
|
-> GROUP BY LEFT(a.transdate, 10) , c.goodsname
|
|
|
|
|
-> ORDER BY LEFT(a.transdate, 10) , c.goodsname;
|
|
|
|
|
+-----------------------+-----------+----------+
|
|
|
|
|
| LEFT(a.transdate, 10) | goodsname | COUNT(*) |
|
|
|
|
|
+-----------------------+-----------+----------+
|
|
|
|
|
| 2020-12-01 | 书 | 1 |
|
|
|
|
|
| 2020-12-01 | 笔 | 1 |
|
|
|
|
|
| 2020-12-02 | 书 | 2 |
|
|
|
|
|
| 2020-12-02 | 笔 | 2 |
|
|
|
|
|
+-----------------------+-----------+----------+
|
|
|
|
|
4 rows in set (0.00 sec)
|
|
|
|
|
```
|
|
|
|
|
- 运行这段代码,我们就得到了每天、每种商品有几次销售的全部结果。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- COUNT(字段)
|
|
|
|
|
- COUNT(字段)用来统计分组内这个字段的值出现了多少次。如果字段值是空,就不统计。
|
|
|
|
|
- 为了说明它们的区别,我举个小例子。假设我们有这样的一个商品信息表,里面包括了商品编号、条码、名称、规格、单位和售价的信息。
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT *
|
|
|
|
|
-> FROM demo.goodsmaster;
|
|
|
|
|
+------------+---------+-----------+---------------+------+------------+
|
|
|
|
|
| itemnumber | barcode | goodsname | specification | unit | salesprice |
|
|
|
|
|
+------------+---------+-----------+---------------+------+------------+
|
|
|
|
|
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
|
|
|
|
|
| 2 | 0002 | 笔 | NULL | 支 | 5.00 |
|
|
|
|
|
| 3 | 0002 | 笔 | NULL | 支 | 10.00 |
|
|
|
|
|
+------------+---------+-----------+---------------+------+------------+
|
|
|
|
|
3 rows in set (0.01 sec)
|
|
|
|
|
```
|
|
|
|
|
- 如果我们要统计字段“goodsname”出现了多少次,就要用到函数 COUNT(goodsname),结果是 3 次:
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT COUNT(goodsname) -- 统计商品名称字段
|
|
|
|
|
-> FROM demo.goodsmaster;
|
|
|
|
|
+------------------+
|
|
|
|
|
| COUNT(goodsname) |
|
|
|
|
|
+------------------+
|
|
|
|
|
| 3 |
|
|
|
|
|
+------------------+
|
|
|
|
|
1 row in set (0.00 sec)
|
|
|
|
|
```
|
|
|
|
|
- 如果我们统计字段“specification”,用 COUNT(specification),结果是 1 次:
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT COUNT(specification) -- 统计规格字段
|
|
|
|
|
-> FROM demo.goodsmaster;
|
|
|
|
|
+----------------------+
|
|
|
|
|
| COUNT(specification) |
|
|
|
|
|
+----------------------+
|
|
|
|
|
| 1 |
|
|
|
|
|
+----------------------+
|
|
|
|
|
1 row in set (0.00 sec)
|
|
|
|
|
```
|
|
|
|
|
- 你可能会问,为啥计数字段“goodsname”的结果是 3,计数字段“specification”却只有 1 呢?其实,这里的原因就是,3 条记录里面的字段“goodsname”没有空值,因此被
|
|
|
|
|
统计了 3 次;而字段“specification”有 2 个空值,因此只统计了 1 次。
|
|
|
|
|
- 理解了这一点,**你就可以利用计数函数对某个字段计数时,不统计空值的特点,对表中字段的非空值进行计数了**。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|