ITPub博客

首页 > 数据库 > 数据库开发技术 > 7个实用示例让你轻松玩转SQL中的GROUP BY

7个实用示例让你轻松玩转SQL中的GROUP BY

原创 数据库开发技术 作者:jackzhang 时间:2023-11-09 19:05:57 0 删除 编辑

摘要:在 SQL 中,GROUP BY 语句与聚合函数结合使用,将相同的数据分配到对应组中。它允许你对一个或多个列中具有相同值的行进行分组,然后对每个组应用聚合函数,以便对分组的数据执行计算。

SQL(结构化查询语言)——是从关系数据库中提取数据并进行转换的一种广泛使用的工具。然而,没有数据聚合,数据转换是不完整的,数据聚合是 SQL 中的一个重要概念。没有 GROUP BY,数据聚合是不可能的!因此,掌握 GROUP BY 从而更容易地执行所有类型的数据转换和聚合非常重要。

GROUP BY 通常与聚合函数(如SUM, AVG, COUNT, MIN, MAX等)一起使用。这些函数对每组中的数据执行计算。例如,你可以通过使用 SUM (sales)和 GROUP BY product 来获得每个产品的销售总额。

但是,为什么聚合函数与groupby一起使用呢?

在 SQL 中,使用 GROUP BY 子句对行进行分组。因此,当对列使用聚合函数时,结果将描述该特定行组的数据。

在这篇文章中,我将通过在 SQL 查询中应用 GROUP BY 子句的7个示例,让你轻松掌握 GROUP BY 的使用。

示例数据表及样本数据准备:
以下是建表语句(数据库为 MySQL 8.1):

-- Table creation
CREATE TABLE dummy_sales_data
(
    order_id         INT AUTO_INCREMENT PRIMARY KEY,
    quantity         INT,
    unit_price       INT COMMENT 'Unit price (USD)',
    status           ENUM ('Delivered', 'Not Delivered', 'Shipped', 'Not Shipped') NOT NULL,
    order_date       DATE,
    product_category VARCHAR(50),
    sales_manager    VARCHAR(20),
    shipping_cost    INT COMMENT 'Shipping cost (USD)',
    delivery_time    INT COMMENT 'Delivery time (days)',
    shipping_address VARCHAR(50),
    product_code     VARCHAR(50),
    order_code       VARCHAR(20)
);

样本数据填充:由于样本数据量较大,通过上述语句建好表之后,直接采用文件导入的方式即可(样本数据获取方式见文末)。数据填充后如下所示:

Ok,话不多说,我们直接上干货!


噢...,还有一点需要强调,在正式开始之前,请牢记使用 GROUP BY 子句的一条规则:

在 SQL 查询中使用 GROUP BY 时,SELECT 语句中的每一列要么出现在 GROUP BY 子句中,要么作为参数出现在聚合函数中。

1. 带聚合函数的 GROUP BY

这是最常用的场景,在一列或多列上应用聚合函数。如上所述,GROUP BY 只是简单地将指定列中具有相似值的行分组在一起。

例如,假设你希望获得每个产品类别的单价统计摘要信息。这个示例详细说明了如何使用所有聚合函数:

SELECT product_category,
       MIN(unit_price) AS lowest_unit_price,
       MAX(unit_price) AS highest_unit_price,
       SUM(unit_price) AS total_unit_price,
       AVG(unit_price) AS average_unit_price
FROM dummy_sales_data
GROUP BY product_category;

正如上述查询所示,我们在其中用到了两列——产品类型(product_category)和单价(unit_price),后者用在聚合函数中(通常是数值列),剩余的一列用在 GROUP BY 中用作分组依据。

另外,你可能会注意到输出结果中有一列的产品类型是 null,这是因为表中的产品类型列存在为空的情况,SQL 默认会将所有空值当作同一种类型的数据,所以会对所有空值进行聚合,当作单个分组。

当然,如果你觉得分组结果中的 null 不好看,也可以给它一个合适的名称,只需要用 COALESCE 即可(当然其他数据库也有专门实现这个功能的函数,比如,SQL Server中的 ISNULL, MySQL 中的 IFNULL)。但是,如果从代码可移植性来讲,推荐使用 COALESCE 函数,因为它是标准 SQL 函数,大多数主流数据库都支持:

SELECT COALESCE(product_category, 'Undefined_category') AS product_category,
       MIN(unit_price)                                  AS lowest_unit_price,
       MAX(unit_price)                                  AS highest_unit_price,
       SUM(unit_price)                                  AS total_unit_price,
       AVG(unit_price)                                  AS average_unit_price
FROM dummy_sales_data
GROUP BY product_category;

这里有一个很重要的点需要注意:我们虽然在 product_category 列上应用了 COALESCE,但实际上并没有聚合该列的值,因为原始数据表中实际并没有这个类型,我们实际聚合的还是所有值为 null 的行。

2. 不带聚合函数的 GROUP BY

尽管大多数时候 GROUP BY 与聚合函数一起使用,但它仍然可以在没有聚合函数的情况下使用——这个时候它的功能其实就是去重或者查找唯一记录,相当于 DISTINCT 关键字的作用。

例如,假设你想检索产品类型(product_category)和状态(status)的所有唯一组合,可以这样做:

SELECT product_category,
       status
FROM dummy_sales_data
GROUP BY product_category, status;

这里,正如我前面所说,有些人可能会说,我在列名前使用 DISTINCT 关键字也可以获得相同的结果,为什么还要用 GROUP BY 子句呢?

然而,基于两个主要原因,说明为什么你应该选择 GROUP BY 而不是 DISTINCT 来获得唯一记录:

  • GROUP BY 子句获得的结果默认按升序排序。因此,你不需要单独对记录进行再排序。
  • 当你在处理具有数百万行的数据集并且 SQL 查询包含连接(JOIN)时,DISTINCT 的成本可能会很高。

因此,使用 GROUP BY 可以高效地从数据库中获取唯一记录,即使在查询中使用多个连接也是如此。

接下来,让我们进一步了解怎么以一种高效的方式限制 GROUP BY 子句的输出。

3. 带 HAVING 子句的 GROUP BY

在 SQL 中,HAVING 和 WHERE 的作用逻辑一样,唯一的区别在于前者是对分组后的结果进行过滤,而后者则是对原始表中的所有记录进行过滤(在数据分析或数据建模岗位的面试中,多数面试官都会问到这个问题)。

例如,假设你想要获取包含产品类别、销售经理和运输成本的唯一记录,其中要求运输成本大于等于34。HAVING 和 WHERE 都可以实现这一目的:

-- WHERE clause
SELECT product_category,
       sales_manager,
       shipping_cost
FROM dummy_sales_data
WHERE shipping_cost >= 34
GROUP BY product_category,
         sales_manager,
         shipping_cost;

-- HAVING clause
SELECT product_category,
       sales_manager,
       shipping_cost
FROM dummy_sales_data
GROUP BY product_category,
         sales_manager,
         shipping_cost
HAVING shipping_cost >= 34;

尽管上述两个查询都能得到相同的输出,但逻辑完全不同。WHERE 子句在 GROUP BY 之前执行,因此实际上它会根据给定条件扫描全表以查找满足条件的记录。

然而,HAVING 在 GROUP BY 之后执行,因此它扫描的记录数量相对较少,因为行已经分组在一起了。所以从性能上讲, HAVING 可以节省更多时间成本。

好吧,退一步说,就算你不太关心效率。但是,现在你需要检索总运输成本超过6000的产品类型和销售经理。这就是 HAVING 派上用场的时候。

在这里,要过滤记录,必须使用条件 SUM(Shipping_Cost) > 6000,因为不能在 WHERE 子句中使用任何聚合函数。

SELECT product_category,
       sales_manager,
       SUM(shipping_cost) AS total_shipping_cost
FROM dummy_sales_data
GROUP BY product_category, sales_manager
HAVING SUM(shipping_cost) > 6000;

由于在 Shipping_Cost 上使用了聚合,因此不需要在 GROUP BY 中使用它。从逻辑上来说,所有行都根据产品类别和销售经理进行分组,然后 HAVING 扫描会基于给定条件扫描这些分组。

因此,HAVING 与 GROUP BY 结合使用是一种基于条件筛选数据的优化方式。

注意:由于 HAVING 在 SELECT 之前执行,所以不能在 HAVING 子句的条件中使用列别名。如果不知道SQL中各个语句的执行顺序,请看我之前的文章(10倍提升你的SQL查询速度)。

此外,尽管 GROUP BY 会默认按升序或字母顺序显示结果,但有时你可能希望按聚合列展示输出结果。这时候,就轮到 ORDER BY 登场了。

4. 带 ORDER BY 子句的 GROUP BY

在 SQL 中,ORDER BY 子句用于对结果集排序,默认排序方式为升序。然而,如果你想获取降序的排序结果,可以在 ORDER BY 子句中的列名后面简单添加一个 DESC 关键字即可。

我们继续上面的例子。你可以看到最后的结果是按升序(字母顺序)排序的,首先是按列 Product_Category 排序,然后再按列 Sales_Manager 排序。但是,最后一列 total_shipping_cost 在排序中并没有用到。

可以通过以下方式按聚合列降序排序输出结果集:

SELECT product_category,
       sales_manager,
       SUM(shipping_cost) total_shipping_cost
FROM dummy_sales_data
GROUP BY product_category, sales_manager
ORDER BY total_shipping_cost DESC;

很明显,现在最后一列现在是按降序排列的。此外,你还可以看到,现在前两列中的值没有特定的顺序。这是因为,你只将它们放在 GROUP BY 中,而没有用在 ORDER BY 子句中。另外,你应该会注意到,在这里,我们在 GROUP BY 子句中使用了列别名,这是因为 GROUP BY 在 SELECT 语句之后执行,所以可以在 GROUP BY 子句中使用列别名。因此,掌握 SQL 中各个子句的执行顺序至关重要(有备无患,小心以后面试官问到你哦<~.~>)。

GROUP BY 子句中可以同时使用多个排序列,且不同列可以应用不同的排序方式。如下所示,第二列我们使用默认的升序排序,第一和最后的聚合列使用降序排序:

SELECT product_category,
       sales_manager,
       SUM(shipping_cost) total_shipping_cost
FROM dummy_sales_data
GROUP BY product_category, sales_manager
ORDER BY product_category DESC,
         sales_manager,
         total_shipping_cost DESC;

学习收获:你可以在 GROUP BY 子句中同时使用多个排序列,且不同列可以应用不同的排序方式,比如按升序排列一些列,其余列按降序排列。但是,要注意在 GROUP BY 中列名的使用顺序,因为不同的顺序会得到不同排列方式的结果集。

在这个示例中,理解 GROUP BY 子句的工作原理至关重要。你有没有问过自己这样一个问题——为什么按降序对 total_shipping_cost 列排序不能看到 shipping_cost 列的所有值呢?

这是因为在 GROUP BY 中只用到了前两列。所有的记录只根据这两列的值进行分组,总成本是通过 shipping_cost 列的值计算得来的。因此,最终,结果集中的总成本值是基于这些分组而不是整个表来排列的。

接下来,我们将继续通过示例进一步帮助你理解记录过滤之间的区别,以及何时应该将 WHERE 和 HAVING 与 GROUP BY 结合使用。

5. 带 WHERE, HAVING 和 ORDER BY 的 GROUP BY

假设你有这样一个需求:希望获得所有未交付给客户的订单的销售经理列表和产品类别。同时,你只想显示在特定产品类别中运输产品成本超过1600美元的销售经理,且最后结果按总运输成本降序输出。

你可可以将这个问题分为以下4步解决:

  • 使用条件 Status = 'Not Delivered' 筛选记录,定位到目标数据。由于这是一个非聚合列,因此可以在 WHERE 中使用。另外,这样可以缩小数据检索范围。

  • 使用条件 SUM(Shipping_Cost) > 1600 根据总运输成本筛选记录。由于这是一个聚合列,因此应该用在 HAVING 子句中。

  • 要计算总运输成本,需要使用 GROUP BY 子句按销售经理和产品类别对前面筛选出来的记录进行分组即可。

  • 在 ORDER BY 子句中使用聚合列并加上 DESC 关键字将结果按降序方式输出。

假设你按照上面的步骤对问题进行拆解,应该可以很容易实现最开始的需求,像下面这样:

SELECT sales_manager,
       product_category,
       SUM(shipping_cost) total_shipping_cost
FROM dummy_sales_data
WHERE status = 'Not Delivered'
GROUP BY sales_manager, product_category
HAVING SUM(shipping_cost) > 1600
ORDER BY total_shipping_cost DESC;

这样就可以成功获得满足示例中提到的条件的销售经理和产品类别的所有唯一组合。

6. 带 WITH ROLLUP 子句的 GROUP BY

在 GROUP BY 子句后增加 WITH ROLLUP 子句可用于对分组聚合结果求小计和总计。例如,假设你想计算每个产品类型和状态组合的总金额,同时想看每个组合的小计及最后的总计。这个时候就可以用 WITH ROLLUP 子句轻松实现:

SELECT COALESCE(product_category, 'Total') product_category,
       COALESCE(status, 'Subtotal')        status,
       SUM(unit_price * quantity)          total_price
FROM dummy_sales_data
WHERE product_category IS NOT NULL
GROUP BY product_category, status
WITH ROLLUP;

这里,小小的调研一下,有多少人不知道 WITH ROLLUP 可以用于对分组再求和的?留言告诉我。因为我工作中有一天给我的领导汇报工作,那天他看到我代码里面写了 WITH ROLLUP,问我什么意思,我说了作用他说他第一次知道还有这功能。哈哈<~.~>!

7. GROUP BY 和 CASE WHEN 语句的组合应用

GROUP BY 和 CASE WHEN 语句的组合常常用于实现复杂的数据提取需求。例如,假设某天产品经理给了你这样的数据提取需求:统计不同价格区间的产品销量和销售金额。这个时候,就是 GROUP BY 和 CASE WHEN 语句协同登场的时候。你可以按以下步骤实现:

  • 首先根据单价字段确定整体价格区间(即最小、最大值),然后再根据区间划分价格区间数量,这个就通过 CASE WHEN 语句实现分类。

  • 然后再使用 GROUP BY 子句对上一步划分的价格区间类型进行聚合求销量和销售金额即可。

由于我们的示例数据中单价在100~250范围内,所以我们简单分为三个价格类型:低价、中等价格、高价。具体实现如下:

SELECT CASE
           WHEN unit_price >= 100 AND unit_price < 150 THEN 'low_price'
           WHEN unit_price >= 150 AND unit_price < 200 THEN 'middle_price'
           ELSE 'high_price'
           END AS                 price_category,
       COUNT(1)                   sale_count,
       SUM(unit_price * quantity) total_price
FROM dummy_sales_data
GROUP BY price_category;

以上就是我想讲的关于 GROUP BY 子句的所有知识点和应用了,如果你还想到别的,请留言与我一起分享!

总结(Summary)
我希望你在读完这篇文章后,能发现它令人耳目一新,并且对你有所帮助。

在过去的几年里,我一直在使用 SQL,根据我的经验,我发现这些方法非常节省时间,而且在处理大型数据集时功能强大。在从事实际项目时,对这些概念的深刻理解非常重要。

说在最后的话
俗话说光说不练都是假把式(哈哈<~.~>),一切理论最后都要付诸实践才能产生它应有的价值。所以我建议你最好能动手跟着做一遍。示例数据获取方式,关注我,回复“group by示例数据”即可获得。
So get your hands dirty, my friends, and good luck!


关注我,一起进入数据的世界,学习相关技术,共同进步,提升自我!

往期看点👉👉👉

关系数据库中的存储过程

掌握SQL:小项目—应用你的SQL技能

掌握SQL:高级主题—视图、存储过程和触发器

掌握SQL:事务和并发—确保数据完整性

掌握SQL:数据库设计—打造高效且可扩展的数据库

原文作者:数据派探险家;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2017-12-19

  • 博文量
    119
  • 访问量
    55977