MySQL必知必会——分组、子查询

一、分组数据

本节介绍如何分组数据,以便能汇总表内容的子集。这涉及两个新SELECT语句子句,分别是GROUP BY子句HAVING子句

1、数据分组

目前为止的所有计算都是在表的所有数据或匹配特定的WHERE子句的数据上进行的。

MariaDB [course]> SELECT COUNT(*) AS num_prods
    -> FROM  products
    -> WHERE vend_id=1003;
+-----------+
| num_prods |
+-----------+
|         7 |
+-----------+
1 row in set (0.00 sec)

上述例子返回供应商1003的所提供的产品数目但是如果要返回每个供应商提供的产品数目还怎么做?或者返回只提供单项产品的供应商所提供的产品,或返回提供10个以上产品的供应商该怎么做?
这时候就该使用分组,分组允许把数据分为多个逻辑组,以便能对每个分组进行聚集计算。

2、创建分组

分组是在SELECT语句的GROUP BY子句中建立的,理解分组的最好办法是看一个例子:

MariaDB [course]> SELECT vend_id,COUNT(*) AS nums_prod
    -> FROM products
    -> GROUP BY vend_id;
+---------+-----------+
| vend_id | nums_prod |
+---------+-----------+
|    1001 |         3 |
|    1002 |         2 |
|    1003 |         7 |
|    1005 |         2 |
+---------+-----------+
4 rows in set (0.00 sec)

上面的SELECT语句指定了两个列,vend_id包含产品提供商的ID,nums_prod为计算字段(用COUNT(*)函数建立)。GROUP BY子句指示MySQL按vend_id排序分组数据。这导致对每个vend_id而不是整个表计算nums_prod一次。
因为使用了GROUP BY,就不必指定要计算和估值的每个组了,系统会自动完成。
GROUP BY子句指示MySQL分组数据,然后对每个组而不是整个结果集进行聚集。


在具体使用GROUP BY子句之前,需要知道这些规定:

  • GROUP BY子句可以包含任意数目的列,这使得能对分组进行嵌套,为数据分组提供更细致的控制
  • 如果在GROUP BY子句中嵌套了分组,数据将在最后规定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(故不能从个别的列取回数据)
  • GROUP BY子句中列出的每个列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在SELECT中使用表达式,则必须在GROUP BY中指定相同的表达式,不能使用别名。
  • 除聚集计算语句外,SELECT语句中的每个列都必须在GROUP BY子句中给出。
  • 如果分组列中具有NULL值,则NULL将作为一个分组返回,如果列中有多行NULL值,它们将分为一组。
  • GROUP BY子句必须出现在WHERE子句之后,ORDER BY子句之前。

ROLLUP:是用来在分组统计数据的基础上再进行统计汇总,即用来得到group by的汇总信息;

MariaDB [course]> SELECT vend_id,COUNT(*) AS  num_prods
    -> FROM products
    -> GROUP BY vend_id WITH ROLLUP;
+---------+-----------+
| vend_id | num_prods |
+---------+-----------+
|    1001 |         3 |
|    1002 |         2 |
|    1003 |         7 |
|    1005 |         2 |
|    NULL |        14 |
+---------+-----------+
5 rows in set (0.00 sec)

3、过滤分组

除了能用GROUP BY分组数据外,MySQL还允许过滤分组,规定包括哪些分组,排除哪些分组。例如,想要列出至少有两个订单的所有顾客,为了得出这种数据 ,必须基于完整的分组而不是个别的行进行过滤。
WHERE子句过滤指定的是行而不是分组,事实上,WHERE并没有分组的概念。
MySQL提供了HAVING子句来对分组进行过滤,HAVING非常类似于WHERE,事实上目前见过的所有类型的WHERE子句都可以使用HAVING子句来替代。唯一的差别是WHERE过滤行,HAVING过滤分组。

MariaDB [course]> SELECT cust_id,COUNT(*) AS order_nums
    -> FROM orders
    -> GROUP BY cust_id
    -> HAVING order_nums >= 2;
+---------+------------+
| cust_id | order_nums |
+---------+------------+
|   10001 |          2 |
+---------+------------+
1 row in set (0.00 sec)

这条SELECT语句的前3行类似于之前的语句,最后一行增加了HAVING子句,它过滤COUNT(*)>=2的那些分组。

HAVING和WHERE的差别:这里有另外一个理解办法,WHERE在数据分组前进行过滤,HAVING在数据分组之后进行过滤。这是一个重要的区别,WHERE排除的行不包括在分组中。这可能会改变计算值,从而影响HAVING子句中基于这些值过滤掉的分组。

假如想进一步过滤上面的语句,使它返回过去12个月内具有两个以上订单的顾客。为达到这一点,可增加一条WHERE子句,过滤出去12个月内下的订单。然后再增加HAVING子句过滤出具有两个以上订单的分组。
为了更好的理解,看下面的例子,它列出具有2个以上的产品且价格为10以上的产品的供应商

MariaDB [course]> SELECT vend_id,COUNT(*) AS num_prods
    -> FROM products
    -> WHERE prod_price >=10
    -> GROUP BY vend_id
    -> HAVING COUNT(*) >= 2;
+---------+-----------+
| vend_id | num_prods |
+---------+-----------+
|    1003 |         4 |
|    1005 |         2 |
+---------+-----------+
2 rows in set (0.00 sec)

这条语句中,第一行是使用了聚集函数的基本SELECT语句,它与前面的例子很像。WHERE子句过滤所有prod_price至少为10的行。然后按找vend_id进行分组数据,HAVING子句过滤计数为2或2以上的分组。如果没有WHERE子句,将会多检索出两行。

MariaDB [course]> SELECT vend_id,COUNT(*) AS num_prods
    -> FROM products
    -> GROUP BY vend_id
    -> HAVING COUNT(*) >= 2;
+---------+-----------+
| vend_id | num_prods |
+---------+-----------+
|    1001 |         3 |
|    1002 |         2 |
|    1003 |         7 |
|    1005 |         2 |
+---------+-----------+
4 rows in set (0.00 sec)

4、分组和排序

虽然GROUP BY和ORDER BY经常完成相同的工作,但它们是非常不同的。下表汇总了它们之间的差别

ORDER BY GROUP BY
排序产生的输出 分组行。单输出可能不是分组的顺序
任意列都可以使用(甚至非选择的列) 只可能使用选择列或表达式,而且必须使用每个选择列表达式
不一定需要 如果聚集函数一起使用列(或表达式),则必须使用

表中的第一项差别极为重要,我们经常发现用GROUP BY分组的数据确实是以分组顺序输出的,但情况并不是这样,它并不是MySQL规范所要求的。此外,用户可能还会要求以不同于分组的顺序排序。仅因为你以某种方式分组数据(获得特定的分组聚集值),并不代表你要以相同的方式排序输出。应该提供明确的ORDER BY子句,即使其效果等同于GROUP BY子句也是如此。

不要忘记ORDER BY子句,一般在使用GROUP BY子句时,应该也给出ORDER BY子句。这是保证数据正确排序的唯一方法,千万不要依赖于GROUP BY排序数据。

为说明GROUP BY和ORDER BY的使用方法,请看一个例子。下面的SELECT语句类似于前面那些例子,它检索总计订单价格大于等于50的订单的订单号和总计订单价格:

MariaDB [course]> SELECT order_num,SUM(item_price*quantity) AS total_price
    -> FROM orderitems
    -> GROUP BY order_num
    -> HAVING SUM(item_price*quantity) >= 50;
+-----------+-------------+
| order_num | total_price |
+-----------+-------------+
|     20005 |      149.87 |
|     20006 |       55.00 |
|     20007 |     1000.00 |
|     20008 |      125.00 |
+-----------+-------------+
4 rows in set (0.00 sec)

为按总计订单价格排序输出,需要添加ORDER BY子句,如下所示:

MariaDB [course]> SELECT order_num,SUM(item_price*quantity) AS total_price
    -> FROM orderitems
    -> GROUP BY order_num
    -> HAVING SUM(item_price*quantity) >= 50
    -> ORDER BY total_price;
+-----------+-------------+
| order_num | total_price |
+-----------+-------------+
|     20006 |       55.00 |
|     20008 |      125.00 |
|     20005 |      149.87 |
|     20007 |     1000.00 |
+-----------+-------------+
4 rows in set (0.00 sec)

在这个例子中,GROUP BY子句用来按订单号(order_num列)分组数据,以便SUM()函数能够返回总计订单价格。HAVING子句过滤数据,使得只返回总计订单价格大于等于50的订单。最后,用ORDER BY子句输出。

5、SELECT子句顺序

下面回顾一下SELECT语句中子句的顺序,下表以在SELECT语句中使用时必须遵循的次序,列出迄今位置所学过的子句。

子句 说明 是否必须
SELECT 要返回的列或表达式
FROM 从中检索数据的表 仅在从表选择数据时使用
WHERE 行级过滤
GROUP BY 分组说明 仅在按组计算聚集时使用
HAVING 组级过滤
ORDER BY 输出排序顺序
LIMIT 要检索的行数

二、使用子查询

1、子查询

SELECT语句是SQL的查询,迄今为止我们看到的所有SELECT语句都是简单查询即从单个数据库表中检索数据的单条语句。

查询(query):任何SQL语句都是查询,但此术语一般指的是SELECT语句。

SQL还允许创建子查询(subquery),即嵌套在其他查询中的查询。

2、利用子查询进行过滤

订单存储在两个表中,对于包含订单号、客户ID、订单日期的每个订单,orders表存储一行。各订单的物品存储在相关的orderitems表中。orders表不存储客户信息,它只存储客户的ID。实际客户的信息存储在customers表中。
现在假如要列出订购物品TNT2的所有客户,下面是具体的步骤。

1. 检索包含物品TNT2的所有订单的编号。
2. 检索具有前一步骤列出的订单编号的所有客户的ID。
3. 检索前一步骤返回的所有客户ID的客户信息。

上述每个步骤都可以单独作为一个查询来执行,可以把一条SELECT语句返回的结果用于另一条SELECT语句的WHERE子句。也可以使用子查询来把3个查询组合成一条语句。

第一条SELECT语句的含义很明确,对于prod_idTNT2的所有订单物品,它检索其order_num时。输出列出两个包含此物品的订单:

MariaDB [course]> SELECT order_num 
    -> FROM orderitems
    -> WHERE prod_id = 'TNT2';
+-----------+
| order_num |
+-----------+
|     20005 |
|     20007 |
+-----------+
2 rows in set (0.00 sec)

下一步,查询具有订单20005和20007的客户ID。

MariaDB [course]> SELECT cust_id
    -> FROM orders
    -> WHERE order_num  IN (20005,20007);
+---------+
| cust_id |
+---------+
|   10001 |
|   10004 |
+---------+
2 rows in set (0.00 sec)

现在把第一个查询(返回订单号)变为子查询组合两个查询。

MariaDB [course]> SELECT cust_id
    -> FROM orders
    -> WHERE order_num  IN ( SELECT order_num
    ->                       FROM orderitems
    ->                       WHERE prod_id = 'TNT2');
+---------+
| cust_id |
+---------+
|   10001 |
|   10004 |
+---------+
2 rows in set (0.00 sec)

在SELECT语句中,子查询总是从内向外来处理。在处理上面的SELECT语句时,MySQL实际上还执行了两个操作。
首先,执行SELECT order_num FROM orderitems WHERE prod_id = 'TNT2'此查询返回两个单号:20005和20007。然后,这 两个值以IN操作符要求的逗号分隔的格式传给外部查询的WHERE子句。外部查询变成了:SELECT cust_id FROM orders WHERE order_num IN (20005,20007)可以看出,输出是正确的并且与前面硬编码WHERE子句所返回的值相同 。
现在得到了订购物品TNT2的所有客户ID,下一步是检索这些客户ID的客户信息,检索两列的SQL语句为:

MariaDB [course]> SELECT cust_name,cust_contact
    -> FROM customers
    -> WHERE cust_id IN (10001,10004);
+----------------+--------------+
| cust_name      | cust_contact |
+----------------+--------------+
| Coyote Inc.    | Y Lee        |
| Yosemite Place | Y Sam        |
+----------------+--------------+
2 rows in set (0.00 sec)

可以把其中的WHERE子句转换为子查询而不是硬编码这些客户ID:

MariaDB [course]> SELECT cust_name,cust_contact
    -> FROM customers
    -> WHERE cust_id IN (SELECT cust_id
    ->                   FROM orders
    ->                   WHERE order_num  IN(SELECT order_num
    ->                                       FROM orderitems
    ->                                       WHERE prod_id = 'TNT2'));
+----------------+--------------+
| cust_name      | cust_contact |
+----------------+--------------+
| Coyote Inc.    | Y Lee        |
| Yosemite Place | Y Sam        |
+----------------+--------------+
2 rows in set (0.00 sec)

为了执行上述的SELECT语句,MySQL实际上执行了3条SELECT语句。最里面的子查询返回订单列表,此列表用于其外面的子查询的WHERE子句。最外面的子查询返回客户ID列表,此客户ID列表用于跟最外层查询的WHERE子句。最外层查询确实返回所需的数据。
可见,在WHERE子句中使用子查询能够编写出功能很强并且灵活SQL语句。对于能嵌套的子查询的数目没有限制,不过在实际使用时由于性能的限制,不能嵌套太多的子查询。

列必须匹配 ,在WHERE子句中使用子查询,应该保证SELECT语句具有与WHERE子句相同数目的列。通常,子查询将返回单个列并且与单个列匹配,但如果需要也可以使用多个列。

虽然子查询一般与IN操作符结合使用,但是也可以用于测试相等(=)、不相等(<>)。

3、作为计算字段使用子查询

使用子查询的另一种方法是创建计算字段,假如需要显示customers表中每个客户的订单总数,订单与相应的客户ID存储在orders表中。
为了执行这个操作,应执行下面步骤:

  1. 从customers表中检索客户列表。
  2. 对于检索出的每个客户,统计其在orders表中的订单数目。

正如之前所述,可使用SELECT COUNT(*)对表中的行进行计数,并且通过提供一条WHERE子句来过滤某个特定的客户ID,可仅对该客户的订单进行计数。例如下面的代码对客户10001的订单进行计数:

MariaDB [course]> SELECT cust_name,
    ->                   cust_state,
    ->                   (SELECT COUNT(*)
    ->                   FROM orders
    ->                   WHERE orders.cust_id = customers.cust_id) AS orders
    -> FROM customers
    -> ORDER BY cust_name;
+----------------+------------+--------+
| cust_name      | cust_state | orders |
+----------------+------------+--------+
| Coyote Inc.    | MI         |      2 |
| E Fudd         | IL         |      1 |
| Mouse House    | OH         |      0 |
| Wascals        | IN         |      1 |
| Yosemite Place | AZ         |      1 |
+----------------+------------+--------+

这条SELECT语句对customers表中每个客户返回3列:cust_name、cust_state、orders。orders是一个计算字段,它是由圆括号中的子查询建立的。该子查询对检索出的每个客户执行一次,在此例子中,该子查询执行了5此,因为检索出了5个客户。
子查询中的WHERE子句与前面使用的WHERE子句稍有不同,因为它使用了完全限定列名。下面的语句告诉SQL比较orders表中的cust_id与当前正从customers表中检索出的cust_id。

 WHERE orders.cust_id = customers.cust_id

相关子查询(corrrelated subquery),涉及到外部查询的子查询。

这种类型的子查询称为相关子查询,任何时候只要列名可能有多义性,就必须使用这种语法(表名和列名由一个句点分隔)。如果不这样的话会如何?请看下例子:

MariaDB [course]> SELECT cust_name,
    ->                   cust_state,
    ->                   (SELECT COUNT(*)
    ->                   FROM orders
    ->                   WHERE cust_id = cust_id) AS orders
    -> FROM customers
    -> ORDER BY cust_name;
+----------------+------------+--------+
| cust_name      | cust_state | orders |
+----------------+------------+--------+
| Coyote Inc.    | MI         |      5 |
| E Fudd         | IL         |      5 |
| Mouse House    | OH         |      5 |
| Wascals        | IN         |      5 |
| Yosemite Place | AZ         |      5 |
+----------------+------------+--------+
5 rows in set (0.00 sec)

显然返回的结果并不正确,因为如果不完全限定列名,MySQL将假定你是对orders表中 的 cust_id进行自身比较,而SELECT COUNT(*) FROM orders WHERE cust_id = cust_id;总是返回orders表中的订单数(因为MySQL查看每个订单的cust_id是否与本身匹配)。
虽然子查询在 构造这种SELECT语句的时候很有用,但是必须注意限制有歧义的列名。

逐渐增加子查询来建立查询:用子查询测试和调试查询很有技巧性,特别是在这些语句的复杂性不断增高的情况下更是如此。用子查询建立查询的最可靠办法是逐渐进行,这与MySQL处理它们的方法非常相同。首先,建立和测试最里层的查询。然后,用硬编码数据建立和测试外层查询,并且仅在确认它正常后才嵌入查询。这时,再次测试它,对于要增加的每个查询,重复这些步骤。这样做仅给构造查询增加了一点点时间,但节省了以后排错的大量时间。并且极大地提高了查询一开始就正常工作地可能性。

发布了114 篇原创文章 · 获赞 30 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/weixin_41476978/article/details/104219564