← 返回文章列表
数据库·562 次阅读

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 条

还没有评论,来抢沙发~

发表评论

评论需审核后显示

© 2026 许冬博客 · 专注 JS 技术分享

·RSS 订阅·Powered by Nuxt 4