一、执行顺序决定一切
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
所以:
WHERE在分组前过滤行,不能用聚合函数。HAVING在分组后过滤组,可以用聚合函数。SELECT里起的别名,WHERE用不了,ORDER BY通常可以用。
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 有多个值,取哪个都不确定。
正确处理:
- 用聚合函数:
MAX(id)、MIN(id)。 - 把列加进
GROUP BY。 - 确实需要「每组任意一条」时,用窗口函数或子查询明确指定。
不要为了省事关掉这个 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;
几个注意点:
SUM对 NULL 返回 NULL,可用COALESCE(SUM(x), 0)。AVG忽略 NULL,分母是非 NULL 的行数,不是总行数。GROUP_CONCAT能把组内值拼成字符串,默认长度限制 1024,可用group_concat_max_len调整。WITH ROLLUP可以追加合计行。
六、性能要点
GROUP BY的列有索引可以避免临时表和排序,EXPLAIN里不应出现Using temporary。- 先
WHERE缩小数据量,再分组。 - 分组列基数过高(如按用户 ID 分组几百万组)会很慢,考虑预聚合或物化视图。
- 大表统计可以放到从库或离线数仓,别在 OLTP 主库上跑。
小结
WHERE 筛行、HAVING 筛组;COUNT(*) 含 NULL,COUNT(列) 不含;非聚合列必须在 GROUP BY 里;取每组最新一条优先用窗口函数;分组列建索引可避免临时表。