mysql数据库分组查询:GROUP BY 与 HAVING 的执行逻辑

同一条分组 SQL,把聚合条件写进 WHERE 直接报错,挪到 HAVING 立刻出结果------不是数据库为难你,是它俩根本不在同一个时间点干活。

这篇我带你顺着执行顺序走一遍,以后拿到任何条件,你都能自己判断它该写在哪个子句里。

一、场景:一张报表里的分组需求

业务侧甩来的统计需求,基本都是同一个模子:

  • 统计每个部门的平均工资;
  • 计算每个商品类别的销售总额;
  • 找出订单数超过 100 的客户;
  • 生成按月汇总的财务报表。

它们的共同点是:统计对象不再是某一行,而是某一类数据。SQL 用分组(GROUP BY)把行切成组,再用聚合函数(Aggregate Function)把每组压成一行;对分组结果的过滤,交给分组过滤(HAVING)。

我第一次写这类报表,需求没多难,却连续踩了三个坑。

二、问题点:三个看似无关的报错

我把当年卡过的写法整理成下表,你可以对照看看自己中过几个。

序号 我写的写法 数据库的反应 我当时的疑惑
1 WHERE AVG(salary) > 10000 直接报错 聚合函数也是函数,为什么不能放 WHERE
2 SELECT dept_id, job ... GROUP BY dept_id 直接报错 job 明明是表里的列,为什么不让选
3 普通条件全塞进 HAVING 能出结果,但很慢 结果明明对,为什么要跑半天

三个现象,一个报错语法、一个报错列、一个拖慢速度,看着毫无关联。

记住:它们的根因只有一个------一条 SQL 不是按你书写的顺序执行的。

三、原理:一条 SQL 的真实执行顺序

那它到底按什么顺序执行?

逻辑执行顺序(Logical Execution Order)如下,注意它是逻辑顺序,不等于书写顺序,也不完全等同于数据库内部的物理执行顺序:

latex 复制代码
FROM -> WHERE -> GROUP BY -> 聚合计算 -> HAVING -> SELECT -> DISTINCT -> ORDER BY -> LIMIT

每一步的动作和数据形态,列成下表:

步骤 干什么 此刻的数据形态
FROM 加载表、做连接 / 笛卡尔积(Cartesian Product) 原始行集合
WHERE 逐行过滤,不满足的丢弃 幸存的原始行
GROUP BY 按分组列把行切成组 多个组,每组多行
聚合计算 对每组算 COUNT/SUM/AVG/MAX/MIN 每组一行
HAVING 淘汰不满足条件的组 剩下的分组
SELECT / DISTINCT 选出列、起别名、去重 结果行
ORDER BY / LIMIT 排序、截取 最终结果

按这条链路,先解释第一个报错。WHERE 是第 2 步,聚合是第 4 步,WHERE 执行时组都还没切,聚合值根本不存在,数据库无从判断。

错误写法(聚合值此刻不存在,直接报错):

sql 复制代码
SELECT dept_id, AVG(salary)
FROM employee
WHERE AVG(salary) > 10000
GROUP BY dept_id;

正确写法(把条件挪到聚合之后的 HAVING):

sql 复制代码
SELECT dept_id, AVG(salary)
FROM employee
GROUP BY dept_id
HAVING AVG(salary) > 10000;

第二个报错同理。分组之后每组只剩一行,可组内的非分组列可能有多个值,数据库不知道该显示哪一个,标准 SQL 直接禁止。

sql 复制代码
SELECT dept_id, job, COUNT(*)
FROM employee
GROUP BY dept_id;  -- job 不在 GROUP BY,也不是聚合,报错

记住:聚合值诞生在 GROUP BY 之后,WHERE 永远够不到它。

四、方案:让每个条件各归其位

拿到一个具体条件,怎么判断它该写哪儿?判断标准就一句话:过滤原始行用 WHERE,过滤分组用 HAVING。

两者的完整差异,列成下表:

维度 WHERE HAVING
执行时机 分组前 聚合后
作用对象 原始行 分组结果
能否用聚合函数 不能 能
能否用分组列 能 能
性能 提前砍行,开销小 分组后才过滤,开销大
是否必须配合 GROUP BY 否 通常配合

落地原则:能在 WHERE 表达的条件,绝不放到 HAVING。提前砍行,参与分组的行数更少,算得更快。

以"统计 2024 年各部门薪资总额,只保留总额大于 50 万的部门"为例:

sql 复制代码
SELECT dept_id, SUM(salary)
FROM employee
WHERE hire_date >= '2024-01-01' AND hire_date < '2025-01-01'
GROUP BY dept_id
HAVING SUM(salary) > 500000;

年份条件放 WHERE,因为它针对原始行;总额条件放 HAVING,因为它是聚合结果。

分组的几个语义细节,我列成下表,写报表时高频用到:

细节 行为
多列分组 GROUP BY dept_id, job 按部门 + 岗位组合键切组
NULL 分组 多个 NULL 会被并成同一组
COUNT(*) 统计所有行,包含 NULL
COUNT(列) 只统计该列非 NULL 的行
SUM/AVG/MAX/MIN 忽略 NULL;整组为 NULL 时 SUM 得 NULL,COUNT 得 0

记住:条件能往前放就别往后拖,行过滤用 WHERE、组过滤用 HAVING。

五、验证:报表写法实测

下面的报表我都在 MySQL 8.0 上实际跑过,你可以照着复现。

客户订单 Top 10:先用 WHERE 限定已支付,再用 HAVING 卡总额。

sql 复制代码
SELECT customer_id, COUNT(*) AS order_cnt, SUM(amount) AS total
FROM orders
WHERE status = 'PAID'
GROUP BY customer_id
HAVING SUM(amount) > 1000
ORDER BY total DESC
LIMIT 10;

条件统计:用 CASE WHEN 做行转列,统计每个部门的高薪、低薪人数。

sql 复制代码
SELECT dept_id,
  SUM(CASE WHEN salary >= 10000 THEN 1 ELSE 0 END) AS high_cnt,
  SUM(CASE WHEN salary < 10000  THEN 1 ELSE 0 END) AS low_cnt
FROM employee
GROUP BY dept_id;

每组取前三名:分组查询会把多行压成一行,取每组 Top N 要靠窗口函数(Window Function)。

sql 复制代码
SELECT dept_id, name, salary
FROM (
  SELECT dept_id, name, salary,
    ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
  FROM employee
) t
WHERE rn <= 3;

PARTITION BY 语义上类似分组,但不会把多行压成一行,所以能保住明细。跑完你会看到类似结果:

latex 复制代码
dept_id  name  salary
1        赵六  24000
1        钱七  18000
1        李四  15000
2        吴九  13000

记住:报表四件套------行过滤 WHERE、组过滤 HAVING、条件统计 CASE WHEN、Top N 用窗口函数。

六、总结延伸:避坑与性能

我把踩过的坑和性能要点分成两张表,发布前扫一眼即可。

常见误区(写之前对照)
WHERE 写聚合函数;SELECT 非分组列;混淆 COUNT(*) 与 COUNT(列);漏看 NULL 被并组;该放 WHERE 的塞进 HAVING
性能建议(数据量大必看)
GROUP BY 列建索引;WHERE 提前过滤;别在分组列上套函数(索引失效,可冗余日期列);HAVING 条件尽量简单;大表用预聚合(汇总表 / 物化视图)

全文链路再收束一次:

先 FROM/WHERE 筛行,再 GROUP BY 切组,然后算聚合,HAVING 筛组,末段 SELECT 出列、ORDER BY 排序、LIMIT 截断。

记住:执行顺序是理解所有 SQL 行为的总钥匙,不用死记语法。

术语速查表

术语 英文 含义
分组 GROUP BY 按分组列把行切成一个个组
聚合函数 Aggregate Function COUNT/SUM/AVG/MAX/MIN,把每组多行压成一行
行过滤 WHERE 分组前对原始行过滤
分组过滤 HAVING 聚合后对分组结果过滤
逻辑执行顺序 Logical Execution Order 数据库处理各子句的先后逻辑
笛卡尔积 Cartesian Product 多表连接时行的组合
窗口函数 Window Function 按分区计算、不压缩行数

参考链接

相关推荐
SelectDB7 小时前
SelectDB 是哪个公司的?是国产数据库吗?一篇讲清飞轮科技与 Apache Doris 的关系
数据库
这个DBA有点耶7 小时前
库内计算推理:一条SQL背后的算子融合,和外挂方案差了几次网络往返
数据库·人工智能·sql
这个DBA有点耶9 小时前
函数索引的“完全一致”有多苛刻:写法、字符集、类型,差一处就失效
数据库·mysql·代码规范
星恒随风9 小时前
MySQL 从零入门(三):表的约束详解
linux·服务器·数据库·笔记·mysql
Alluxio10 小时前
Alluxio + OCI:为 AI 工作负载实现亚毫秒访问延迟与可扩展吞吐
运维·数据库·人工智能·分布式·缓存·ai·语言模型
数据库急诊室_阿哲14 小时前
一次加索引建议的复核记录:从报告到签字
数据库·ai数据库
青瓦梦滋14 小时前
Redis主从复制
服务器·数据库·redis·缓存·bootstrap
慧都小妮子15 小时前
Wyn商业智能国产数据库适配:数据接入、分析缓存与审计日志验证清单
数据库·数据分析·数据可视化·审计日志·商业智能
Joy T16 小时前
Redis Lua 原子限流:Lua 脚本、竞态条件与并发控制
数据库·redis·lua·限流·竞态条件·redis scripting
杨凯凡16 小时前
MySQL 死锁怎么排查?InnoDB 锁等待与 Spring Boot 整事务重试实战
数据库·spring boot·mysql