GROUP BY 与 HAVING:SQL 聚合查询的正确姿势
这周写报表 SQL,被 GROUP BY 坑了一把:WHERE 里引用了聚合函数,MySQL 直接报错。查文档才发现自己对聚合查询的执行顺序理解一直是模糊的。整理成笔记,顺便把 HAVING 的定位彻底搞清楚。
先建立心智模型:执行顺序
SQL 语句的书写顺序和执行顺序是两回事。聚合查询的实际执行顺序:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
记住这个链条,很多"为什么报错"就自然有答案了:
- WHERE 在 GROUP BY 之前执行 → WHERE 里不能用聚合函数(那时还没分组,哪来的聚合值)
- HAVING 在分组之后执行 → HAVING 才能用 SUM、COUNT 这些
- SELECT 在最后 → WHERE/HAVING 里不能用 SELECT 起的别名(MySQL 对 HAVING 放宽了,但别依赖)
基础:分组统计
文章表 posts(id, category, views, status),统计每个分类的文章数:
SELECT category, COUNT(*) AS post_count
FROM posts
GROUP BY category
ORDER BY post_count DESC;
+-----------+------------+
| category | post_count |
+-----------+------------+
| vue | 18 |
| js | 15 |
| css | 5 |
+-----------+------------+
常用聚合函数:COUNT、SUM、AVG、MAX、MIN。
COUNT 的细节:
SELECT
COUNT(*) AS c1, -- 所有行
COUNT(views) AS c2, -- views 非 NULL 的行
COUNT(DISTINCT category) AS c3 -- 去重后的分类数
FROM posts;
COUNT(*) 和 COUNT(列) 的区别就是那个 NULL,统计前想清楚要不要算 NULL 行。
HAVING:分组后的筛选
需求升级:只显示文章数超过 5 的分类。
-- ❌ 错误:WHERE 阶段还没分组,COUNT 无从谈起
SELECT category, COUNT(*) AS post_count
FROM posts
WHERE COUNT(*) > 5
GROUP BY category;
-- ✅ 正确:分组之后用 HAVING 筛
SELECT category, COUNT(*) AS post_count
FROM posts
GROUP BY category
HAVING COUNT(*) > 5;
WHERE + HAVING 组合拳
两者经常一起用,各司其职:
-- 统计"已发布"文章中,文章数超过 5 的分类
SELECT category, COUNT(*) AS post_count
FROM posts
WHERE status = 'PUBLISHED' -- 先筛行:只统计已发布
GROUP BY category -- 再分组
HAVING COUNT(*) > 5 -- 最后筛组:只要超过 5 篇的
ORDER BY post_count DESC;
记忆口诀:WHERE 筛行,HAVING 筛组。能用 WHERE 的条件就别放 HAVING——WHERE 先执行,过滤掉的数据不进分组,性能更好。
经典坑:SELECT 里的非聚合列
SELECT category, title, COUNT(*)
FROM posts
GROUP BY category;
这条 SQL 想干什么?每个分类分组了,但一个分类下有好多 title,显示哪一个?
- MySQL 5.7 之前:不报错,随机给你一行的 title(埋雷)
- MySQL 5.7+ 默认开启
ONLY_FULL_GROUP_BY:直接报错,要求 SELECT 的非聚合列必须出现在 GROUP BY 里
这是好事,把暧昧写法挡在门外。正确姿势——想要每分类下阅读量最高的文章,用子查询或窗口函数:
-- MySQL 8.0 窗口函数方案
SELECT category, title, views
FROM (
SELECT
category, title, views,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY views DESC) AS rn
FROM posts
) t
WHERE rn = 1;
实战:多个常用统计模板
按月统计发文量:
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
COUNT(*) AS cnt
FROM posts
WHERE status = 'PUBLISHED'
GROUP BY month
ORDER BY month;
统计分类的同时带上总阅读量:
SELECT
category,
COUNT(*) AS post_count,
SUM(views) AS total_views,
AVG(views) AS avg_views,
MAX(views) AS max_views
FROM posts
GROUP BY category
HAVING total_views > 10000
ORDER BY total_views DESC;
找出重复数据(比如 slug 重复的隐患排查):
SELECT slug, COUNT(*) AS cnt
FROM posts
GROUP BY slug
HAVING cnt > 1;
最后一个特别好用,脏数据排查必备。
小结
- 执行顺序:WHERE(筛行)→ GROUP BY(分组)→ HAVING(筛组)
- WHERE 里不能用聚合函数,对聚合结果的筛选一律用 HAVING
- 能 WHERE 的别 HAVING,先筛行再分组性能更好
- ONLY_FULL_GROUP_BY 模式下,SELECT 的非聚合列必须在 GROUP BY 里
GROUP BY ... HAVING COUNT(*) > 1是排查重复数据的神器
聚合查询是 SQL 从"增删改查"走向"数据分析"的第一步。下一步准备啃窗口函数,那才是 MySQL 8.0 给的真正大礼。
看到这里了?如果这篇文章帮到过你
请我喝瓶可乐就好 🥤

评论0 条
还没有评论,来抢沙发~