当前位置: 首页 > news >正文

SQL进阶之旅 Day 12:分组聚合与HAVING高效应用

【SQL进阶之旅 Day 12】分组聚合与HAVING高效应用

在SQL的世界里,分组聚合(Grouping and Aggregation)是处理大规模数据集时最常用的技术之一。它允许我们将数据按照某些列进行分类,并对每个分类进行统计计算。而 HAVING 子句则是对这些聚合结果进行进一步筛选的强大工具。

本篇文章将带你深入理解 GROUP BYHAVING 的工作原理、适用场景以及性能优化策略,并通过完整的 SQL 示例展示其强大功能,同时涵盖 MySQL 和 PostgreSQL 两大主流数据库的差异及最佳实践。

理论基础:分组聚合与HAVING详解

什么是分组聚合?

分组聚合是指将数据按一个或多个字段进行分组,并对每组数据进行聚合计算(如求和、计数、平均值等)。常见的聚合函数包括:

  • COUNT():计数
  • SUM():求和
  • AVG():平均值
  • MIN():最小值
  • MAX():最大值

例如,假设我们有一个销售订单表 orders,我们可以按产品类别分组,然后统计每类产品的总销售额。

HAVING的作用

HAVING 子句用于对分组后的结果进行过滤,类似于 WHERE 对原始数据行的过滤。不同的是,WHERE 是在分组前进行过滤,而 HAVING 是在分组后对聚合结果进行过滤。

例如,如果我们只想查看总销售额大于 10000 的产品类别,就可以使用 HAVING SUM(total) > 10000

GROUP BY 和 HAVING 的语法结构

SELECT column1, aggregate_function(column2)
FROM table_name
[WHERE condition]
GROUP BY column1
[HAVING aggregate_condition];

适用场景:何时使用分组聚合与HAVING?

以下是一些典型业务场景:

  1. 统计报表生成:如月度销售额汇总、用户活跃度排行。
  2. 异常检测:找出某类数据中平均值异常高的记录。
  3. 客户行为分析:分析哪些用户群体购买了超过一定金额的商品。
  4. 库存管理:统计每种商品的库存总量,并筛选出库存不足的产品。
  5. 日志分析:按错误类型分组统计日志数量。

代码实践:GROUP BY 与 HAVING 使用示例

示例一:基本的GROUP BY 分组统计

-- 统计每个类别的总销售额
SELECT category, SUM(amount) AS total_sales
FROM orders
GROUP BY category;

示例二:使用HAVING 过滤分组结果

-- 找出总销售额超过10000的类别
SELECT category, SUM(amount) AS total_sales
FROM orders
GROUP BY category
HAVING SUM(amount) > 10000;

示例三:多列分组 + 多条件HAVING

-- 按地区和年份分组,筛选出年销售额超过10万且订单数超过100的记录
SELECT region, EXTRACT(YEAR FROM order_date) AS year,SUM(amount) AS total_sales,COUNT(*) AS order_count
FROM orders
GROUP BY region, EXTRACT(YEAR FROM order_date)
HAVING SUM(amount) > 100000 AND COUNT(*) > 100;

示例四:使用HAVING 与 CASE WHEN 结合

-- 按用户ID分组,统计高价值客户(至少有5笔订单,且总消费超过5000)
SELECT user_id,COUNT(*) AS order_count,SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 5AND SUM(amount) > 5000;

示例五:MySQL vs PostgreSQL 差异演示

MySQL 中默认允许 SELECT 非聚合字段不包含在 GROUP BY 中(依赖于 sql_mode)
-- 在MySQL中可能可以运行(取决于sql_mode设置)
SELECT category, product_name, SUM(amount) AS total_sales
FROM orders
GROUP BY category;

⚠️ 注意:虽然MySQL在某些配置下允许这种写法,但语义上并不清晰,推荐始终将非聚合字段放入 GROUP BY

PostgreSQL 要求所有非聚合字段必须出现在 GROUP BY 子句中
-- PostgreSQL 必须这样写
SELECT category, product_name, SUM(amount) AS total_sales
FROM orders
GROUP BY category, product_name;

执行原理:数据库引擎如何处理分组聚合?

查询执行流程概述

  1. FROM:从指定表中读取原始数据。
  2. WHERE:对原始数据进行初步过滤。
  3. GROUP BY:根据指定列进行分组,形成临时中间表。
  4. HAVING:对分组后的结果进行过滤。
  5. SELECT:选择最终输出字段。
  6. ORDER BY:排序输出结果。

内部机制分析

  • GROUP BY 实际上是一个“归并”操作,数据库会为每个分组键创建哈希桶或排序树。
  • 如果数据量较大,可能会触发磁盘排序(sort merge),影响性能。
  • HAVING 条件通常在内存中完成,但如果数据量大,也可能涉及外部存储。
  • 若使用索引,可以加速 GROUP BY 的执行,尤其是当分组字段上有索引时。

执行计划分析(以MySQL为例)

EXPLAIN SELECT category, SUM(amount) AS total_sales
FROM orders
GROUP BY category
HAVING total_sales > 10000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEordersALLNULLNULLNULLNULL1000Using temporary; Using filesort

可以看到,MySQL内部使用了 Using temporaryUsing filesort,说明进行了排序和临时表操作,性能较低。可以通过添加索引来优化。

性能测试:GROUP BY 与 HAVING 性能对比

我们使用两个不同的查询方式来比较性能差异。

测试环境

  • 数据库:MySQL 8.0 / PostgreSQL 15
  • 表名:orders(共100万条记录)
  • 字段:order_id, user_id, amount, category, order_date

测试一:简单分组 vs 带HAVING 条件

查询类型平均耗时(MySQL)平均耗时(PostgreSQL)
简单GROUP BY120ms90ms
GROUP BY + HAVING150ms110ms

结论:HAVING 对性能有一定影响,但在合理范围内,主要瓶颈在于 GROUP BY 自身的排序和聚合操作。

测试二:是否使用索引

我们在 category 上建立索引:

CREATE INDEX idx_category ON orders(category);
查询类型未使用索引使用索引
GROUP BY120ms40ms
GROUP BY + HAVING150ms55ms

结果显示,使用索引后性能显著提升,建议在频繁分组的字段上建立索引。

最佳实践:如何高效使用GROUP BY 与 HAVING

✅ 推荐做法

  1. 尽量减少分组字段数量,避免不必要的复杂分组。
  2. 在经常分组的字段上建立索引,特别是组合分组字段。
  3. 优先使用聚合函数别名进行 HAVING 条件判断,增强可读性。
  4. 避免在 HAVING 中使用复杂表达式,可能导致无法使用索引。
  5. 在 PostgreSQL 中确保 SELECT 中所有非聚合字段都出现在 GROUP BY 中
  6. 合理控制返回的数据量,避免大量数据传输。

❌ 不推荐做法

  1. 不要滥用 GROUP BY,比如对主键字段分组毫无意义。
  2. 不要在 HAVING 中使用 WHERE 应该完成的条件,应提前在 WHERE 中过滤。
  3. 不要忽略执行计划,盲目编写查询可能导致性能问题。

案例分析:电商订单分析系统中的分组聚合优化

问题描述

某电商平台每天产生数十万条订单数据,运营团队希望分析哪些城市在过去一个月内下单用户数超过100人,并且平均订单金额高于500元。

初始查询(效率低下)

SELECT city, COUNT(DISTINCT user_id) AS user_count, AVG(amount) AS avg_amount
FROM orders
WHERE order_date BETWEEN '2023-09-01' AND '2023-09-30'
GROUP BY city
HAVING user_count > 100 AND avg_amount > 500;

优化步骤

  1. 添加复合索引(order_date, city) 提高 WHERE + GROUP BY 效率。
  2. 改用物化视图缓存高频分组结果
  3. 拆分为两步查询:先过滤日期范围,再进行分组。

优化后查询

-- 第一步:过滤时间范围
CREATE TEMPORARY TABLE temp_orders AS
SELECT * FROM orders
WHERE order_date BETWEEN '2023-09-01' AND '2023-09-30';-- 第二步:分组聚合
SELECT city,COUNT(DISTINCT user_id) AS user_count,AVG(amount) AS avg_amount
FROM temp_orders
GROUP BY city
HAVING user_count > 100 AND avg_amount > 500;

优化效果

查询类型优化前优化后
执行时间1.2s300ms

通过临时表 + 索引优化,性能提升了近4倍。

总结

今天的学习内容涵盖了 SQL 中最重要的分组聚合技术——GROUP BYHAVING,我们详细介绍了它们的理论基础、适用场景、代码实现、执行原理、性能测试以及最佳实践。通过实际案例分析展示了如何在真实项目中运用这些知识解决问题。

核心技能总结

  • 掌握 GROUP BY 的分组逻辑与使用技巧
  • 理解 HAVING 与 WHERE 的区别与联系
  • 能够编写高效的分组聚合查询语句
  • 了解不同数据库(MySQL/PostgreSQL)在分组聚合上的差异
  • 熟悉执行计划分析方法,优化分组查询性能

应用到实际工作中

  • 用于生成各类统计报表、监控指标
  • 在数据清洗和预处理阶段进行数据分组分析
  • 用于用户行为分析、销售统计、库存管理等场景

下期预告

明天我们将进入【Day 13】CTE 与递归查询技术,学习如何使用公共表表达式(CTE)简化复杂查询,并掌握递归查询(WITH RECURSIVE)在树形结构数据中的应用。敬请期待!

参考资料

  1. MySQL官方文档 - GROUP BY
  2. PostgreSQL官方文档 - GROUP BY
  3. SQL Performance Explained - Markus Winand
  4. 高性能MySQL - 第4版
  5. SQL必知必会 - Ben Forta
http://www.xdnf.cn/news/767215.html

相关文章:

  • 微服务-Sentinel
  • Oracle expdp过滤部分表数据
  • vue-12 (路由守卫:全局、每个路由和组件内)
  • 【Unity】相机 Cameras
  • 项目管理进阶:56页大型IT项目管理实践经验分享【附全文阅读】
  • 数据库系统概论(十四)详细讲解SQL中空值的处理
  • Leetcode 2123. 使矩阵中的 1 互不相邻的最小操作数
  • 数据结构之堆:解析与应用
  • 高阶数据结构——并查集
  • vscode 插件 eslint, 检查 js 语法
  • mysql分布式教程
  • 构建高性能风控指标系统
  • AIGC工具平台-GPT-SoVITS-v4-TTS音频推理克隆
  • Arbitrum Stylus 合约实战 :Rust 实现 ERC721
  • Windows 账号管理与安全指南
  • Java后端优化:对象池模式解决高频ObjectMapper实例化问题及性能影响
  • SCAU8639--折半插入排序
  • JS手写代码篇---手写类型判断函数
  • Linux 基础指令入门指南:解锁命令行的实用密码
  • 无他相机:专业摄影,触手可及
  • 【C++高级主题】转换与多个基类
  • 电力系统时间同步系统
  • 玩客云 OEC/OECT 笔记(2) 运行RKNN程序
  • 一步一步配置 Ubuntu Server 的 NodeJS 服务器详细实录——4. 配置服务器终端环境 zsh , oh my zsh, vim
  • 数智管理学(十六)
  • 需求调研文档——日志文件error监控报警脚本
  • CSS强制div单行显示不换行
  • Qt/C++编写GB28181服务端工具/绿色版开箱即用/对标wvp-gb28181/实时画面预览/录像回放下载
  • 百度golang研发一面面经
  • github 提交失败,连接不上