极简工具箱

免费在线工具 · 本地计算 · 无需上传

GROUP BY 与 HAVING:分组统计的五个常用写法

`WHERE` 不能用聚合函数,`HAVING` 可以——但很多人把过滤条件写错地方,导致先分组再过滤,白白多算了几百万行。

广告位(百度联盟)

一、执行顺序决定一切

FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT

所以:

SELECT user_id, COUNT(*) AS cnt
FROM orders
WHERE created_at >= '2026-01-01'   -- 先筛行
GROUP BY user_id
HAVING COUNT(*) > 5                -- 再筛组
ORDER BY cnt DESC;

把 created_at 条件写到 HAVING 里虽然语法上可能通过(如果它出现在 GROUP BY 里),但会让所有行都参与分组,性能差很多。

二、COUNT(*)、COUNT(列)、COUNT(1)

写法含义
COUNT(*)统计行数,包含 NULL 行
COUNT(1)同上,语义等价
COUNT(col)统计该列非 NULL 的行数
COUNT(DISTINCT col)去重计数

InnoDB 里 COUNT(*) 和 COUNT(1) 性能基本一致,优化器会选最小的索引来扫。COUNT(col) 因为要判断 NULL,略慢,且结果可能不同——如果 col 有 NULL,数量会少。

三、only_full_group_by 报错

SELECT id, user_id, COUNT(*) FROM orders GROUP BY user_id;
-- ERROR 1055: Expression #1 of SELECT list is not in GROUP BY clause

MySQL 5.7 起默认开启 ONLY_FULL_GROUP_BY:SELECT 里的非聚合列必须出现在 GROUP BY 中。这是正确行为——按 user_id 分组后,id 有多个值,取哪个都不确定。

正确处理:

不要为了省事关掉这个 SQL mode,那会让查询结果变得不可预测。

四、分组取每组最新一条

这是最经典的需求,三种写法:

写法一:子查询取最大 ID

SELECT o.* FROM orders o
JOIN (
  SELECT user_id, MAX(id) AS max_id FROM orders GROUP BY user_id
) t ON t.max_id = o.id;

写法二:窗口函数(MySQL 8.0+,推荐)

SELECT * FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
  FROM orders
) t WHERE rn = 1;

写法三:关联子查询(数据量小时可读性好)

SELECT * FROM orders o
WHERE created_at = (
  SELECT MAX(created_at) FROM orders WHERE user_id = o.user_id
);

写法二最灵活(能取前 N 条),写法一在数据量大时性能更稳定。注意写法三遇到「同一时间有多条」会返回多行。

五、其它常用聚合

SELECT
  DATE(created_at) AS d,
  COUNT(*) AS orders,
  SUM(amount) AS total,
  AVG(amount) AS avg_amount,
  COUNT(DISTINCT user_id) AS buyers
FROM orders
GROUP BY d
ORDER BY d;

几个注意点:

六、性能要点

小结

WHERE 筛行、HAVING 筛组;COUNT(*) 含 NULL,COUNT(列) 不含;非聚合列必须在 GROUP BY 里;取每组最新一条优先用窗口函数;分组列建索引可避免临时表。

本文相关工具(本地计算,无需上传)

更多使用教程