ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SQL GROUP BY 与 HAVING 用法详解:从分组统计到性能优化

SQL GROUP BY 与 HAVING 用法详解:从分组统计到性能优化 SQL 里的 GROUP BY 和 HAVING是数据库管理系统日常开发与数据分析岗位面试里最高频的两个子句也是从“会查表”走向“会统计”的分水岭。很多人能背出语法但一遇到“用 WHERE 还是 HAVING”“为什么 HAVING 不能单独使用”“GROUP BY 之后能不能 SELECT 其他列”这类问题时就卡壳。本文将用一份结构化员工表作为测试数据从语法、执行顺序、聚合函数配合、性能优化到常见报错排查完整拆解 GROUP BY 和 HAVING 的用法。读者看完本文能够直接在自己的 MySQL、PostgreSQL、SQL Server 或 SQLite 环境中验证所有示例并能回答绝大多数数据库管理系统相关的 SQL 分组统计面试题。先看核心能力速览再逐步展开语法细节和实操验证。1. GROUP BY 与 HAVING 核心能力速览能力项说明所属概念SQL 查询语句中的分组子句与分组过滤子句常用数据库MySQL、PostgreSQL、SQL Server、Oracle、SQLite、DM 等GROUP BY 作用按一个或多个列对查询结果分组配合聚合函数做统计HAVING 作用对 GROUP BY 生成的分组结果进行条件过滤与 WHERE 区别WHERE 在分组前过滤行HAVING 在分组后过滤分组是否必须搭配 GROUP BYHAVING 通常与 GROUP BY 一起使用单独使用场景较少典型聚合函数COUNT、SUM、AVG、MAX、MIN、GROUP_CONCAT方言函数执行顺序FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT适用场景分组统计、报表分析、数据清洗、面试笔试、业务看板常见报错“not functionally dependent on columns in GROUP BY clause”性能要点分组列建议建立索引尽量避免对无索引列做大分组统计从能力速览可以看出GROUP BY 解决的是“如何分组统计”HAVING 解决的是“如何过滤统计结果”两者组合后才构成完整的聚合查询能力。2. 适用场景与使用边界2.1 适合什么场景GROUP BY 和 HAVING 最典型的场景是数据统计。业务开发中下面几类需求几乎每天都要写统计每个部门的员工人数按dept_name分组COUNT(*)统计人数。统计每个部门的平均薪资按dept_name分组AVG(salary)计算平均值。筛选平均薪资超过阈值的部门先用GROUP BY dept_name分组再用HAVING AVG(salary) 10000过滤。统计每月入职人数按DATE_FORMAT(hire_date, %Y-%m)分组再COUNT(*)。统计订单表中每个客户的累计消费金额按customer_id分组SUM(amount)求总额。找出重复数据按业务主键分组HAVING COUNT(*) 1定位重复记录。数据分析师、后端开发、运维人员在写报表 SQL 和接口查询时都会高频用到这两个子句。2.2 不建议用在什么场景GROUP BY 不是万能的。以下场景需要谨慎数据量特别大且无分组索引时GROUP BY 会触发全表扫描和临时表排序性能可能很差。业务需要保留分组内明细数据时GROUP BY只能输出分组列和聚合结果明细行被折叠此时更合适的是窗口函数ROW_NUMBER()、PARTITION BY或子查询。需要跨分组计算占比、同比环比时单独使用 GROUP BY 不够灵活建议配合窗口函数SUM(...) OVER (PARTITION BY ...)。字符串拼接、按组去重等需求存在数据库方言差异例如 MySQL 的GROUP_CONCAT、PostgreSQL 的STRING_AGG、SQL Server 的FOR XML PATH迁移时要注意。2.3 使用边界与规范提醒在真实数据库管理系统中操作数据时有一点必须强调如果分组统计涉及用户实名信息、手机号、订单金额等敏感数据测试环境要使用脱敏数据不能把生产库的原始敏感记录直接导出到个人电脑。SQL 本身是通用技术但数据访问权限、隐私保护和合规边界需要由执行者自行把控。3. SQL 执行环境准备本文示例以 MySQL 语法为主同时说明 PostgreSQL 和 SQL Server 的差异。读者不需要专门安装完整数据库也可以使用 SQLite 内存库、在线 SQL 练习平台或本地 Docker 容器验证。推荐几种准备方式# 方式一Docker 快速启动 MySQL 8 docker run --name mysql-test -e MYSQL_ROOT_PASSWORD123456 -p 3306:3306 -d mysql:8# 方式二Docker 快速启动 PostgreSQL 16 docker run --name pg-test -e POSTGRES_PASSWORD123456 -p 5432:5432 -d postgres:16如果本机已经有 MySQL 或 SQL Server 实例直接使用现有实例即可。创建测试库和测试表CREATE DATABASE IF NOT EXISTS sql_demo DEFAULT CHARACTER SET utf8mb4; USE sql_demo; CREATE TABLE employees ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_name VARCHAR(50) NOT NULL, dept_name VARCHAR(50) NOT NULL, salary DECIMAL(10, 2) NOT NULL, hire_date DATE NOT NULL );插入测试数据INSERT INTO employees (emp_name, dept_name, salary, hire_date) VALUES (张三, 技术部, 15000.00, 2021-03-15), (李四, 技术部, 18000.00, 2020-07-01), (王五, 产品部, 12000.00, 2022-01-10), (赵六, 产品部, 11000.00, 2021-11-20), (孙七, 产品部, 16000.00, 2019-05-06), (周八, 运营部, 9000.00, 2023-02-14), (吴九, 运营部, 9500.00, 2022-08-30), (郑十, 技术部, 22000.00, 2018-09-12), (钱十一, 运营部, 8500.00, 2024-01-05);后续所有示例都基于这张表。表中包含 4 个部门技术部 3 人、产品部 3 人、运营部 3 人薪资覆盖 8500 到 22000。4. GROUP BY 子句基础用法4.1 基本语法GROUP BY 子句用于将查询结果按照一个或多个列进行分组。SELECT 分组列, 聚合函数(统计列) FROM 表名 WHERE 行级过滤条件 GROUP BY 分组列;核心逻辑是数据库先把满足 WHERE 条件的行取出来然后按照 GROUP BY 指定的列把相同的值归为一组最后对每组执行聚合函数计算。4.2 单列分组统计每个部门的员工人数SELECT dept_name, COUNT(*) AS emp_count FROM employees GROUP BY dept_name;执行结果dept_nameemp_count技术部3产品部3运营部3SELECT dept_name, SUM(salary) AS total_salary, AVG(salary) AS avg_salary, MAX(salary) AS max_salary, MIN(salary) AS min_salary FROM employees GROUP BY dept_name;执行结果dept_nametotal_salaryavg_salarymax_salarymin_salary技术部55000.0018333.333322000.0015000.00产品部39000.0013000.000016000.0011000.00运营部27000.009000.00009500.008500.00从结果可以看到GROUP BY 把原始 9 行数据折叠成了 3 个分组每个分组输出一行统计结果。这就是“分组聚合”的本质。4.3 多列分组实际业务经常需要按多个维度分组例如“按部门和年份统计人数”。SELECT dept_name, YEAR(hire_date) AS hire_year, COUNT(*) AS emp_count FROM employees GROUP BY dept_name, YEAR(hire_date) ORDER BY dept_name, hire_year;多列分组的规则是先按第一列分组再按第二列分组只有组合值完全相同的行才归入同一组。这种写法在订单报表、用户分群统计中非常常见。4.4 GROUP BY 的 SELECT 列限制这里要重点说明一个坑。在标准 SQL 和 MySQL 8 默认配置下SELECT 中出现的非聚合列必须出现在 GROUP BY 子句中。-- 错误示例emp_name 不在 GROUP BY 中 SELECT dept_name, emp_name, COUNT(*) FROM employees GROUP BY dept_name;这条语句如果直接执行可能报错或者在旧 MySQL 版本中会随机返回某个员工的姓名。原因是一个分组里有多个 emp_name数据库不知道该返回哪一个。正确做法是只选择分组列和聚合函数SELECT dept_name, COUNT(*) FROM employees GROUP BY dept_name;PostgreSQL 和 SQL Server 在这点上非常严格只要 SELECT 中出现非分组非聚合列直接报错。建议所有环境下都遵循“SELECT 的列要么在 GROUP BY 里要么被聚合函数包裹”的规范。5. HAVING 子句用法5.1 HAVING 解决的问题WHERE 子句无法过滤聚合函数的结果。例如需求是“找出平均薪资超过 10000 的部门”写成下面这样会报错-- 错误示例WHERE 中不能直接使用聚合函数 SELECT dept_name, AVG(salary) AS avg_salary FROM employees WHERE AVG(salary) 10000 GROUP BY dept_name;WHERE 是逐行过滤过滤发生在分组之前此时聚合函数还没有计算结果因此不允许使用AVG(salary)这种聚合条件。正确写法是使用 HAVINGSELECT dept_name, AVG(salary) AS avg_salary FROM employees GROUP BY dept_name HAVING AVG(salary) 10000;执行结果dept_nameavg_salary技术部18333.3333产品部13000.0000运营部平均薪资 9000被过滤掉了。5.2 HAVING 单独使用HAVING 不强制要求前面必须有 GROUP BY。在 MySQL 中可以写SELECT COUNT(*) AS total_emp FROM employees HAVING COUNT(*) 5;此时整张表被当作一个大分组COUNT(*) 返回 9条件成立。但这种写法可读性较差等价于WHERE COUNT(*) 5的语义而标准 SQL 中更推荐使用 WHERE 或子查询。日常开发建议始终让 HAVING 与 GROUP BY 配合出现语义更清晰。5.3 HAVING 中可以使用多个聚合条件HAVING 支持多个条件的组合也可以使用别名。例如筛选员工数大于 2 人、平均薪资大于 8000 的部门SELECT dept_name, COUNT(*) AS emp_count, AVG(salary) AS avg_salary FROM employees GROUP BY dept_name HAVING COUNT(*) 2 AND AVG(salary) 8000 ORDER BY avg_salary DESC;执行结果dept_nameemp_countavg_salary技术部318333.3333产品部313000.0000运营部39000.0000HAVING 中也可以使用 SELECT 中的别名SELECT dept_name, AVG(salary) AS avg_salary FROM employees GROUP BY dept_name HAVING avg_salary 10000;MySQL 支持在 HAVING 中引用别名但 PostgreSQL 和 SQL Server 对别名的解析顺序不同部分场景可能需要重复写聚合表达式。为了可移植性建议直接写完整聚合表达式。6. WHERE 与 HAVING 的区别这部分是面试重点也是日常开发最容易混淆的地方。对比维度WHEREHAVING过滤时机分组之前过滤行分组之后过滤分组能否使用聚合函数不能可以是否必须搭配 GROUP BY否可独立使用通常搭配 GROUP BY性能影响先缩小数据量通常更高效分组计算后再过滤数据量大时开销高与 SELECT 别名关系通常不能使用 SELECT 别名部分数据库支持执行顺序先执行后执行用一个组合查询演示两者同时出现需求统计 2022 年之前入职的员工中每个部门平均薪资大于 12000 的部门。SELECT dept_name, AVG(salary) AS avg_salary FROM employees WHERE hire_date 2022-01-01 GROUP BY dept_name HAVING AVG(salary) 12000;先由 WHERE 过滤掉 2022 年之后入职的员工再由 GROUP BY 分组最后用 HAVING 筛掉平均薪资不达标的部门。需要注意WHERE 和 HAVING 的执行顺序决定了同一个条件写在两个位置效果可能不同。例如“统计部门人数大于 1”的部门若在 WHERE 中写COUNT(*) 1会直接报错若在 WHERE 中写普通行过滤条件则会先减少参与分组的行数影响最终分组结果。所以结论是行级条件放 WHERE分组级条件放 HAVING。7. SQL 子句执行顺序理解执行顺序是写出正确 SQL 的关键。一个完整查询的标准执行顺序为FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT逐层拆解序号子句作用1FROM确定数据来源表2WHERE过滤原始行剔除不满足条件的记录3GROUP BY对过滤后的行进行分组4HAVING过滤分组结果5SELECT计算选择列、聚合函数、表达式6ORDER BY对最终结果排序7LIMIT限制返回行数这个顺序解释了三个常见问题为什么 WHERE 不能使用聚合函数因为执行到 WHERE 时还没分组。为什么 HAVING 可以使用聚合函数因为执行到 HAVING 时已经完成分组和聚合计算。为什么 ORDER BY 可以使用别名因为执行到 ORDER BY 时 SELECT 已经计算完成。在 MySQL 中GROUP BY 隐式使用顺序排序如果不需要排序可以在 GROUP BY 后面加ORDER BY NULL来避免额外排序开销MySQL 5.7 之前的版本有优化价值MySQL 8 基本不再需要。PostgreSQL 不会对 GROUP BY 结果隐式排序需要排序时显式加 ORDER BY。8. 高级分组统计场景8.1 分组后按聚合结果排序统计每个部门的平均薪资并按平均薪资降序排列SELECT dept_name, AVG(salary) AS avg_salary FROM employees GROUP BY dept_name ORDER BY avg_salary DESC;注意这里 ORDER BY 中使用的是 SELECT 别名avg_salary在 MySQL 中可以直接使用。如果 ORDER BY 中使用聚合函数写法为ORDER BY AVG(salary) DESC;8.2 分组后限制返回数量按部门分组统计总薪资取薪资最高的两个部门SELECT dept_name, SUM(salary) AS total_salary FROM employees GROUP BY dept_name ORDER BY total_salary DESC LIMIT 2;这条语句在 MySQL、PostgreSQL、SQLite 中都支持。SQL Server 不支持LIMIT需要改写为SELECT TOP 2 ... ORDER BY total_salary DESC。8.3 分组后查找重复数据数据清洗场景非常常用。假设员工表里 emp_name 允许重复要找出重名员工SELECT emp_name, COUNT(*) AS cnt FROM employees GROUP BY emp_name HAVING COUNT(*) 1;因为当前测试数据没有重名所以结果为空。如果有一张订单表想找相同订单号重复记录直接换成GROUP BY order_no HAVING COUNT(*) 1即可。8.4 按时间维度分组统计按月统计入职人数SELECT DATE_FORMAT(hire_date, %Y-%m) AS hire_month, COUNT(*) AS emp_count FROM employees GROUP BY DATE_FORMAT(hire_date, %Y-%m) ORDER BY hire_month;PostgreSQL 写法为TO_CHAR(hire_date, YYYY-MM)SQL Server 写法为FORMAT(hire_date, yyyy-MM)。时间分组的方言差异在跨数据库迁移时需要单独处理。8.5 条件聚合在 GROUP BY 内部用CASE WHEN做条件聚合可以一次查出多个指标SELECT dept_name, COUNT(*) AS total_count, SUM(CASE WHEN salary 12000 THEN 1 ELSE 0 END) AS high_salary_count FROM employees GROUP BY dept_name;执行结果dept_nametotal_counthigh_salary_count技术部33产品部32运营部30这种方式在报表 SQL 中非常实用一次遍历即可完成多条件统计效率比多次子查询更高。8.6 结合窗口函数做组内排名如果希望在分组统计的同时保留每条记录的组内排名可以使用窗口函数SELECT emp_name, dept_name, salary, ROW_NUMBER() OVER (PARTITION BY dept_name ORDER BY salary DESC) AS rank_in_dept FROM employees;这里PARTITION BY和GROUP BY不同它不折叠行而是保持明细数据不变同时计算组内排名。当 GROUP BY 无法满足“既要明细又要组内统计”的需求时窗口函数是更好的选择。9. 性能优化建议9.1 索引与 GROUP BYGROUP BY 本质上分两步先扫描数据再对分组列排序或使用哈希分组。MySQL 8 使用哈希分组后性能优于早期版本但如果分组列没有索引数据量增大后依然可能产生临时文件和磁盘排序。一个通用的优化原则是为 GROUP BY 的分组列建立联合索引。CREATE INDEX idx_emp_dept ON employees(dept_name, hire_date);如果查询经常按部门和入职时间分组这个联合索引可以直接覆盖分组列减少排序开销。如果还要带 WHERE 条件例如WHERE hire_date 2022-01-01 GROUP BY dept_name索引设计需要遵循最左前缀原则。9.2 WHERE 前移先通过 WHERE 缩小参与分组的数据集再分组性能最好。例如下面两条语句在数据量大时性能差异明显-- 推荐先过滤再分组 SELECT dept_name, AVG(salary) FROM employees WHERE hire_date 2022-01-01 GROUP BY dept_name;-- 不推荐分组后再过滤临时表数据量更大 SELECT dept_name, AVG(salary) FROM employees GROUP BY dept_name HAVING MIN(hire_date) 2022-01-01;二者的语义并不完全等价但表达了一个核心思路能用 WHERE 提前过滤的行不要留到 HAVING 阶段处理。9.3 避免不必要的分组列GROUP BY 后面每多一列分组数可能成倍增加内存和临时表开销也会增大。写报表 SQL 前先确认是否真的需要多维分组。如果只需要总记录数直接SELECT COUNT(*) FROM table比GROUP BY分组再数更快。9.4 使用 EXPLAIN 观察执行计划MySQL 中使用EXPLAIN查看 GROUP BY 是否触发临时表和文件排序EXPLAIN SELECT dept_name, COUNT(*) FROM employees GROUP BY dept_name;重点观察 Extra 列是否出现Using temporary和Using filesort。如果出现说明当前分组列的索引利用不充分需要考虑调整索引或改写查询。9.5 分批处理避免大分组超大表的聚合统计一次性执行容易造成长事务和锁竞争。实际工程中建议按时间范围分批统计再把结果合并。例如按年循环统计月订单量然后插入汇总表。这种方式牺牲了实时性但稳定性更高。10. 常见问题与排查方法问题现象可能原因排查方式解决方案报错“not functionally dependent on columns in GROUP BY clause”SELECT 中包含了非分组列和非聚合列检查 SELECT 列表将非分组列加入 GROUP BY或包裹在聚合函数中WHERE 中使用聚合函数报错SQL 语法不允许在 WHERE 中使用聚合函数查看错误语句位置把聚合条件移到 HAVINGHAVING 中使用 SELECT 别名在某些数据库不生效不同数据库对 SELECT 别名解析顺序不同查看数据库版本文档在 HAVING 中写完整聚合表达式GROUP BY 结果顺序不稳定MySQL 8 不再对 GROUP BY 隐式排序多次执行对比结果显式添加 ORDER BY分组后想要的明细数据丢失GROUP BY 会折叠行检查业务需求改用窗口函数或子查询GROUP BY 查询特别慢分组列无索引或未先 WHERE 过滤执行 EXPLAIN 查看执行计划为分组列建索引WHERE 条件前置COUNT(1) 和 COUNT(*) 结果不一致表中存在 NULL 值列对比不同计数列明确统计目标COUNT(列名) 不包含 NULL 行使用 GROUP_CONCAT 时字符串过长被截断group_concat_max_len 参数限制查看 MySQL 参数执行 SET SESSION group_concat_max_len 100000下面重点拆解两个高频报错。第一个高频报错是“is not functionally dependent on columns in GROUP BY clause”。这个问题在 MySQL 8、PostgreSQL 和 SQL Server 中都会出现。原因就是 SELECT 列表里出现了既不是分组列、也没有被聚合函数包裹的列。解决方式只有两种把该列加入 GROUP BY或者用ANY_VALUE()、MAX()、MIN()等函数包起来。不过更推荐思考业务是否真的需要取这个列。第二个高频问题是“WHERE 和 HAVING 用混”。典型表现是过滤聚合结果时习惯性写在 WHERE然后报语法错误。记住执行顺序即可WHERE 是先过滤原始行HAVING 是后过滤分组。如果条件里出现了COUNT、SUM、AVG、MAX、MIN这个条件就只能放 HAVING。11. 最佳实践11.1 分组列后置放 SELECT 最右侧阅读和调试时建议先看分组列是什么再看聚合指标是什么。保持 SELECT 中第一列是分组列后面依次是聚合列可读性最好SELECT dept_name, COUNT(*) AS emp_count, ROUND(AVG(salary), 2) AS avg_salary FROM employees GROUP BY dept_name;11.2 注意 NULL 分组GROUP BY 会把 NULL 值单独作为一组。如果分组列存在 NULL统计结果中会出现一个NULL分组。需要过滤时可以用HAVING dept_name IS NOT NULL或提前在 WHERE 中过滤。11.3 聚合结果重命名聚合函数返回的列名通常很长且不直观。使用别名AS avg_salary可以方便后续排序和程序解析结果集。别名在 ORDER BY 中可以使用在 HAVING 中建议谨慎使用完整表达式。11.4 多环境验证不同数据库管理系统对 GROUP BY 和 HAVING 的语法兼容性有差异。同样的语句在 MySQL 能跑在 PostgreSQL 或 SQL Server 不一定能跑。团队协作时建议约定统一使用标准 SQL 写法避免依赖某个数据库的宽松语法。例如不要依赖 MySQL 对非聚合列的宽松处理尽量做到 SELECT 列全部规范化。11.5 输出目录与脚本管理如果经常写分组统计 SQL建议把常用统计脚本做成数据库视图或存储过程避免重复粘贴长 SQL。例如可以创建一个部门统计视图CREATE VIEW v_dept_stats AS SELECT dept_name, COUNT(*) AS emp_count, AVG(salary) AS avg_salary FROM employees GROUP BY dept_name;后续查询直接SELECT * FROM v_dept_stats WHERE avg_salary 10000;开发效率更高也便于统一口径。12. 总结GROUP BY 和 HAVING 是 SQL 聚合查询的基石。GROUP BY 负责把数据按维度折叠成组HAVING 负责对分组结果做二次过滤二者配合聚合函数来完成绝大多数统计需求。理解执行顺序是掌握这两个子句的关键FROM 先取数WHERE 先过滤行GROUP BY 完成分组HAVING 过滤分组最后 SELECT 输出结果。写 SQL 时记住一个原则行级条件放 WHERE分组条件放 HAVINGSELECT 中要么是分组列要么是聚合列就不会再出现语法混淆。建议读者把本文的表结构和 12 条示例 SQL 全部在自己的数据库管理系统中跑一遍重点关注三点单列分组与多列分组的区别、WHERE 与 HAVING 的执行顺序差异、以及 GROUP BY 对 SELECT 列的限制。跑通之后再尝试优化部门统计查询的索引用 EXPLAIN 观察执行计划变化。本文的示例可以直接作为面试前的 SQL 复习资料收藏备用即可。
返回列表