SQL 分组统计的 3 种实现方式,含完整示例与避坑

猿友 2026-09-28 10:41:33 浏览数 (24)
反馈

SQL 分组统计看着只有 GROUP BY 一句,实际写法差别很大。本文用建表插样的最小示例,讲清 GROUP BY 搭配聚合函数、GROUP BY 加 HAVING 过滤、以及窗口函数三种实现方式的适用边界,重点说明为什么 WHERE 和 HAVING 不能混用。核心结论是:先按行过滤用 WHERE,过滤完再按组判断用 HAVING,两者顺序固定,写反了结果就错。下面每种写法都给出可直接执行的语句、预期输出与常见失败。先确认数据库版本和表结构,再决定用哪种。

SQL 分组统计三种实现方式对比示意图

一、先看结论:三种实现方式怎么选

下面这张表给出三种写法的适用场景和注意点。判断顺序是:要不要保留未分组的明细行、过滤条件是不是跟聚合结果有关。

实现 适用场景 注意点
GROUP BY + 聚合函数 按类别汇总,明细不需要 SELECT 里非聚合列须出现在 GROUP BY 中
GROUP BY + HAVING 对汇总结果再加条件 HAVING 不能替代 WHERE,两者阶段不同
窗口函数 保留每行,同时给分组内数值 写法复杂,适合排行榜与同比环比

三种方式适用场景与过滤阶段对照图

需要提醒的是,WHERE 在分组前执行、HAVING 在分组后执行,这个顺序决定了同一个条件放错位置结果就不同。想在线对比不同写法的输出,可用 在线代码实例 验证实际结果。

二、环境确认与最小示例

先建表插样,保证每条语句都能对着真实数据验证。示例基于 MySQL 与标准 SQL,主流关系库行为一致;数据库版本差异主要影响窗口函数支持,MySQL 8.0 起可用。以下为预期结果说明,未在本机执行,按数据库官方文档推断。

-- 建表:订单表,按状态与月份做分组统计
CREATE TABLE orders (
  order_id   INT PRIMARY KEY AUTO_INCREMENT,
  user_name  VARCHAR(20),
  amount     DECIMAL(10,2),
  status     VARCHAR(10),
  created_at DATE
);

-- 插入样例数据,覆盖两个用户、两种状态、三个月份
INSERT INTO orders (user_name, amount, status, created_at) VALUES
  (\'张三\', 120.00, \'paid\',   \'2026-09-01\'),
  (\'张三\', 200.00, \'paid\',   \'2026-09-15\'),
  (\'张三\',  80.00, \'refund\', \'2026-09-20\'),
  (\'李四\', 150.00, \'paid\',   \'2026-08-10\'),
  (\'李四\',  90.00, \'paid\',   \'2026-08-25\');

建表之后先跑一条合计,确认数据落库再往下写。这一步的检查很关键,能避免后面用错数据验证分组结果。SQL 版本差异对 GROUP BY 的基本语义没有影响,但「非聚合列不出现在 GROUP BY 中」这条规则在宽松模式下会被容忍,属于必须靠版本确认的边界。

三、实现一:GROUP BY 加聚合函数

第一种是最常用的汇总:按某一列分组,对每组做求和、计数或均值。

-- 实现一:按用户分组,统计订单数与总金额
SELECT user_name,
       COUNT(*)      AS order_cnt,   -- 每组订单条数
       SUM(amount)   AS total_amount -- 每组金额合计
FROM orders
GROUP BY user_name;
-- 预期返回两行:张三 2 单 300.00,李四 2 单 240.00

判定标准是「明细行要不要出现在结果里」。不需要就分组汇总,需要就别用 GROUP BY,改用窗口函数。常见失败是 SELECT 里放了一列没写进 GROUP BY 的字段,结果该列取值不确定,实际表现为随机取组内某一行的值,这类失败在开启宽松模式时最容易漏过。

四、实现二:GROUP BY 加 HAVING

第二种在汇总之后再加条件。区分方法很简单:条件是针对单行的还是针对整组的。

-- 实现二:按用户分组,只保留订单数不少于 2 的组
SELECT user_name,
       COUNT(*)    AS order_cnt,
       SUM(amount) AS total_amount
FROM orders
GROUP BY user_name
HAVING COUNT(*) >= 2;   -- HAVING 作用在分组结果上
-- 预期返回张三、李四两行,两条记录都满足不少于 2 单

-- 对照:把同样的条件放进 WHERE,结果会完全不同
SELECT user_name, COUNT(*) AS order_cnt
FROM orders
WHERE COUNT(*) >= 2      -- 报错:WHERE 里不能出现聚合函数
GROUP BY user_name;

第二条语句说明了一个硬规则:WHERE 阶段还没分组,聚合函数不能用。修复方向是把它挪到 HAVING。HAVING 可以引用聚合结果,也可以引用别名,两种写法都成立。

五、实现三:窗口函数保留明细

第三种保留所有明细行,同时给出分组内的汇总值,适合排行榜和同环比。

-- 实现三:保留每行明细,同时给出该用户的累计金额
SELECT order_id,
       user_name,
       amount,
       SUM(amount) OVER (PARTITION BY user_name) AS user_total
FROM orders
ORDER BY order_id;
-- 每行都保留,user_total 为该用户所在组的合计

PARTITION BY 决定按哪列分组,ORDER BY 在这里只影响展示顺序。这类写法的失败点在于:很多人以为窗口函数会减少行数,实际它不改变行数,与 GROUP BY 的语义差别很大。想系统补齐 SQL 基础,可以看 SQL 教程。如果同一份数据既要明细又要汇总,窗口函数比先分组再自连接省事得多。

现象 常见原因 修复方向
报错聚合函数不能用 把聚合条件写进了 WHERE 挪到 HAVING,WHERE 只做分组前行过滤
非聚合列取值不确定 SELECT 有列没写进 GROUP BY 把该列补进 GROUP BY 或改用聚合函数
统计条数比预期多 用了 COUNT(列) 遇到空值 改用 COUNT(*) 统计行数
以为窗口函数会减少行数 混淆了 GROUP BY 与窗口函数 窗口函数保留全部明细行,不合并

以上失败与修复均来自真实边界场景,未在本机执行,按数据库官方文档推断。

总结

SQL 分组统计的实现选择有一条清晰主线:只要汇总结果、不要明细,用 GROUP BY 加聚合函数;汇总完再按组过滤,用 HAVING;要保留明细同时带分组数值,用窗口函数。过滤条件的放置遵循固定顺序,WHERE 在分组前过滤行,HAVING 在分组后过滤组,写反了不是报错就是结果错。动手前先确认数据库版本是否支持窗口函数,再按上面三种方式对号入座。

延伸学习

想在编程狮系统学 SQL 分组与聚合,可以顺着下面三篇深入:

常见问题

Q:WHERE 和 HAVING 到底有什么区别?

这里有个容易混的细节:COUNT(*) 与 COUNT(列名) 的差别在空值上。统计行数用前者,统计某列非空的数量用后者,两者混用会让同一份数据给出两个不同的条数。再看 SUM,它遇到 NULL 会跳过而不是当成 0,某一组若全是 NULL,结果就是 NULL 而不是 0,前端拿到后再做加法就会出错。要不要用 COALESCE(SUM(列), 0) 兜住,取决于展示层能不能接受 NULL,这一条在金额汇总场景里几乎必踩。

A:执行阶段不同。WHERE 在分组前过滤原始行,不能出现聚合函数;HAVING 在分组后过滤组,可以引用聚合结果。判断条件针对单行还是针对整组,就知道该放哪。

Q:SELECT 里非聚合列没写进 GROUP BY 会怎样?

A:结果不确定,数据库可能取组内任意一行的值,也可能报错,取决于数据库配置。这类问题不会显式提示,属于最隐蔽的失败,写完后务必核对输出。

Q:统计行数该用 COUNT(*) 还是 COUNT(列)?

A:统计行数用 COUNT(*)。COUNT(列) 会跳过该列为 NULL 的行,如果该列存在空值,统计结果会比实际行数少。

Q:窗口函数会像 GROUP BY 一样把多行合成一行吗?

A:不会。窗口函数保留所有明细行,只在每行上附加一个分组内的计算值。要合并行必须用 GROUP BY,两者语义不同。还有一处与窗口函数配套的写法值得记住:窗口函数里可以再带一个 ORDER BY 做组内排序。例如 ROW_NUMBER() OVER (PARTITION BY user_name ORDER BY amount DESC) 会给每个用户的订单按金额从高到低编号,取编号等于 1 的就是该用户最大一笔订单,这类写法在排行榜场景里比自连接省事得多。要注意窗口函数在 MySQL 8.0 之前不可用,老版本只能先分组再关联,动手前先确认版本。

0 人点赞