Mysql聚合函数使用笔记

COUNT使用

COUNT支持两种形式:

  • COUNT(*),计算行数
  • COUNT(expr), 列名或表达式,计算非空值的个数

注意:

COUNT(*) with no WHERE clause performs a full table scan.

如果表超大,可以使用这个命令得到近似值:

SELECT TABLE_ROWS FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'cookbook' AND TABLE_NAME = 'states';

灵活利用COUNT(expr),可以实现多种不同的计算:

mysql> select*from driver_log;
+--------+-------+------------+-------+
| rec_id | name  | trav_date  | miles |
+--------+-------+------------+-------+
|      1 | Ben   | 2014-07-30 |   152 |
|      2 | Suzi  | 2014-07-29 |   391 |
|      3 | Henry | 2014-07-29 |   300 |
|      4 | Henry | 2014-07-27 |    96 |
|      5 | Ben   | 2014-07-29 |   131 |
|      6 | Henry | 2014-07-26 |   115 |
|      7 | Suzi  | 2014-08-02 |   502 |
|      8 | Henry | 2014-08-01 |   197 |
|      9 | Ben   | 2014-08-02 |    79 |
|     10 | Henry | 2014-07-30 |   203 |
+--------+-------+------------+-------+
10 rows in set (0.01 sec)

计算trav_date分别为周末和工作日的次数:
DAYOFWEEK是从星期天算起的,为1,所以周末是1和6;
WEEKDAY是从周一算起的,为0,所以周末是5和6;

mysql> SELECT
    ->   COUNT(IF(DAYOFWEEK(trav_date) IN (1,7),1,NULL)) AS 'weekend trips',
    ->   COUNT(IF(DAYOFWEEK(trav_date) IN (1,7),NULL,1)) AS 'weekday trips'
    ->   FROM driver_log;
+---------------+---------------+
| weekend trips | weekday trips |
+---------------+---------------+
|             4 |             6 |
+---------------+---------------+
1 row in set (0.01 sec)

mysql> SELECT
    ->   COUNT(IF(WEEKDAY(trav_date) IN (5,6),1,NULL)) AS 'weekend trips',
    ->   COUNT(IF(WEEKDAY(trav_date) IN (5,6),NULL,1)) AS 'weekday trips'
    ->   FROM driver_log;
+---------------+---------------+
| weekend trips | weekday trips |
+---------------+---------------+
|             4 |             6 |
+---------------+---------------+
1 row in set (0.00 sec)

聚合函数与WHERE

聚合函数不能用于WHERE子句中,下面语句无法执行:

SELECT COUNT(*),name FROM driver_log WHERE COUNT(*)> 3 GROUP BY name;

因为WHERE子句是用于声明选择哪些行的,而聚合函数的值必须在行选择完之后才能确定。

解决办法就是把聚合函数放到HAVING子句中,因为HAVING操作的对象是分组(already-selected-and-grouped set of rows),而不是行。如果在HAVING中有聚合函数表达式条件,表示最终选择行结果时,聚合函数的结果必须满足这个条件。

SELECT COUNT(*),name FROM driver_log GROUP BY name HAVING COUNT(*)> 3 ;

聚合函数与GROUP BY

聚合函数一般与GROUP BY一起使用,达到更好的分类统计效果。
然而,使用时有个陷阱:

如果SELECT子句中的任一表达式包含非聚合列,同时该列又没有在Group By子句中出现,则会报非功能性依赖错误。

举个例子。问题:对driver_log表中的每个司机,输出miles最大值及其对应的当天日期。
例如,对于上表中的示例数据,我们人工计算结果如下:

+--------+-------+------------+-------+
| rec_id | name  | trav_date  | miles |
+--------+-------+------------+-------+
|      1 | Ben   | 2014-07-30 |   152 |   ---->这个是Ben的最大记录
|      2 | Suzi  | 2014-07-29 |   391 |
|      3 | Henry | 2014-07-29 |   300 |  ---->这个是Henry的最大记录
|      4 | Henry | 2014-07-27 |    96 |
|      5 | Ben   | 2014-07-29 |   131 |
|      6 | Henry | 2014-07-26 |   115 |
|      7 | Suzi  | 2014-08-02 |   502 |  ---->这个是Suzi的最大记录
|      8 | Henry | 2014-08-01 |   197 |
|      9 | Ben   | 2014-08-02 |    79 |
|     10 | Henry | 2014-07-30 |   203 |
+--------+-------+------------+-------+

分析:“对每个”这种语境,一般就要Group By,所以很容易想到如下查询:

SELECT name, trav_date, MAX(miles) AS 'longest trip' FROM driver_log GROUP BY name;

但是,运行时却报错了:

ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'cookbook.driver_log.trav_date' 
which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

SELECT列表中的第2个表达式(即trav_date)没有出现在GROUP BY子句中,而且这个表达式包含非聚合列(这里也是指trav_date列),造成该表达式没有功能性依赖GROUP BY子句中的列,这与sql_mode=only_full_group_by不兼容。

mysql> select @@sql_mode;
+-----------------------------------------------------------------------------------------------------------------------+
| @@sql_mode                                                                                                            |
+-----------------------------------------------------------------------------------------------------------------------+
| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+-----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

先去掉这个限制:重新设置一下这个字符串,去掉前面的ONLY_FULL_GROUP_BY

mysql> set @@sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
Query OK, 0 rows affected (0.00 sec)

mysql> select @@sql_mode;
+----------------------------------------------------------------------------------------------------+
| @@sql_mode                                                                                         |
+----------------------------------------------------------------------------------------------------+
| STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+----------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

重新执行上面的查询语句:

mysql> SELECT name, trav_date, MAX(miles) AS 'longest trip' FROM driver_log GROUP BY name;
+-------+------------+--------------+
| name  | trav_date  | longest trip |
+-------+------------+--------------+
| Ben   | 2014-07-30 |          152 |
| Suzi  | 2014-07-29 |          502 |
| Henry | 2014-07-29 |          300 |
+-------+------------+--------------+
3 rows in set (0.00 sec)

对比发现第3行的结果不正确!日期不对:

    | Suzi  | 2014-07-29 |   502 |
| 7 | Suzi  | 2014-08-02 |   502 |

为什么?

This happens because when you include a GROUP BY clause in a query, the only values that you can meaningfully select are the grouping columns or summary values calculated from the groups.

因为在使用GROUP BY子句时,我们能查询到的有意义的值要么是分组的列,要么是从分组中计算来的聚合值。也就是说我们要查询的值必须功能性依赖于GROUP BY子句中的列。

If you display additional table columns, they’re not tied to the grouped columns and the values displayed for them are indeterminate. (For the statement just shown, it appears that MySQL may simply be picking the first date for each driver, regardless of whether it matches the driver’s maximum mileage value.)

如果我们使用了额外的列或基于该列的表达式,它们就不能与被分组的列绑定,导致它们的值没法确定。所以上面例子中的日期(理论上)都是错误的,MySQL只是简单地把每个司机最早出现的日期返回。

本质上是由SQL语句执行顺序引起的,GROUP BY会先执行,SELECT后执行。GROUP BY执行的时候,根据分组字段,将具有相同分组字段的记录归并成一条记录。因为每一个分组只能返回一条记录,除非是被过滤掉了,而不在分组字段里面的字段可能会有多个值,多个值是无法放进一条记录的,所以必须通过聚合函数将这些具有多值的列转换成单值。

所以SQL规定:

使用GROUP BY时,SELECT 子句中的选择列表中不能包含被分组列和聚合列以外的列。

其实不仅是SELECT子句,HAVING 和ORDER BY也是在GROUP BY之后执行的,也要符合上面的规定

如何解决?

解决方案有很多,具体可参考MySQL官方文档:
The Rows Holding the Group-wise Maximum of a Certain Column

这里先介绍最简单的一种,使用临时表。临时表对我们做实验分析时还是很有用的,但在实际应用开发时很少用到。

临时表的方案很简单:把不包含日期的查询结果先保存在临时表里,再用一个内联结(INNER JOIN,条件值两边必须同时存在)查询就可以了。

CREATE TEMPORARY TABLE t 
	SELECT name, MAX(miles) AS miles FROM driver_log GROUP BY name;

SELECT d.name, d.trav_date, d.miles AS 'longest trip' 
	FROM driver_log AS d INNER JOIN t USING (name, miles) ORDER BY name;

还有一种方法是使用非相关子查询:

SELECT t1.name, trav_date, t1.miles
FROM driver_log t1
JOIN (
  SELECT name, MAX(miles) AS miles
  FROM driver_log
  GROUP BY name) AS t2
ON t1.name = t2.name AND t1.miles = t2.miles
ORDER BY name;

其思路是:

  • 首先,t2 是按name分组的最大miles,所以t2包含所有司机的最大里程。尤其注意MAX(miles) AS miles,这使得t2表中的列名与t1表中的miles相同;
  • 然后再用t1内联结t2,条件是name和miles都必须相同。

还有一种方法是使用LEFT JOIN:

SELECT t1.name, t1.trav_date, t1.miles
FROM driver_log t1
LEFT JOIN driver_log t2 ON t1.name = t2.name AND t1.miles < t2.miles
WHERE t2.name IS NULL
ORDER BY t1.name;

其思路是巧妙运用列左联结的特性:使用左联结时,在ON执行完筛选之后,会把左边表中不符合条件的数据保留,而右边表中对应的值全部设置为NULL。

注意上面查询语句的执行顺序,

  1. 执行FROM,对t1和t2执行笛卡尔积,结果存虚拟表VT1
  2. 执行ON筛选,符合条件的结果存VT2
  3. 执行LEFT JOIN,对2中不符合的行,左表中的值不变,右表中的值全部为NULL,并将这些行加到2中,存VT3。对上面十条记录的表而言,只有三条这样的数据,并且这三条数据就是三个司机对应的最大的miles里程。因为自己跟自己比较,找不到比自己小的任何行,不就是说明我是最大的么。
  4. 执行WHERE,由上面分析可知,右边name为NULL,就是筛选最大值。
  5. 执行ORDER BY(其实问题中并没有要求排序输出)。
  6. 执行SELECT,总共其实是有8列(左右各4列)的,我们只需要左表中的name,trav_date,miles即可以满足条件。

猜你喜欢

转载自blog.csdn.net/wen524/article/details/88357967