分组分页时总数必须明确是组数还是原始记录总和:组数需用子查询或cte统计分组结果行数,如select count() from (select dept_id from emp group by dept_id) t;原始记录总和则用窗口函数sum(count()) over()或两层聚合。

分组分页时不能直接用 COUNT(*) 拿总组数,必须先明确“总数”指什么:是分组后的组数,还是每组内原始记录数之和?两者 SQL 写法完全不同,搞错就查不到想要的结果。
分组后有多少组(即分组总数)
这是最常被误当成“分页总数”的需求。比如按部门分组查员工数,你想知道一共查出了几个部门——这其实是 GROUP BY 后的行数,不是原始表总记录数。
- 不能在分页主查询里加
COUNT(*),否则会把分组结果再聚合为一行 - 正确做法是用子查询或 CTE 先完成分组,再对结果集计行:
SELECT COUNT(*) FROM (SELECT dept_id FROM emp GROUP BY dept_id) t;
- 如果还要分页(比如只取前 10 个部门),这个总数必须单独查一次,不能和分页数据共用一个查询
分组分页时每页数据对应的原始记录总数
比如“第 2 页显示 10 个部门,每个部门下有若干员工”,你可能真正想看的是这 10 个部门共包含多少员工(即所有匹配记录的总和),而不是部门个数。
- 此时不能只靠
SUM(COUNT(*))—— 它语法非法;得用窗口函数或两层聚合 - 推荐写法(MySQL 8.0+ / PostgreSQL / SQL Server):
SELECT dept_id, COUNT(*) AS emp_cnt, SUM(COUNT(*)) OVER() AS total_emp_cnt FROM emp GROUP BY dept_id ORDER BY dept_id LIMIT 10 OFFSET 10;
- 旧版 MySQL(SUM 结果得先算出再关联,否则会因
GROUP BY失效
带 WHERE 条件的分组总数统计容易漏掉过滤逻辑
很多人把条件写在分页外层,导致总数和分页数据不一致。例如:
- 错误写法(总数没过滤,分页数据过滤了):
SELECT dept_id, COUNT(*) FROM emp GROUP BY dept_id HAVING COUNT(*) > 5 LIMIT 10;
→ 这里的COUNT(*)总数若从全表算,就和实际分页展示的组不匹配 - 正确做法:所有筛选条件(
WHERE和HAVING)必须在子查询中统一应用,总数和分页数据基于同一数据集 - 特别注意
HAVING是对分组后结果的筛选,它影响组数,但不参与原始记录计数
MySQL 中用 SQL_CALC_FOUND_ROWS 不再推荐
老项目里常见这种写法:SELECT SQL_CALC_FOUND_ROWS ... LIMIT 10,再跟 SELECT FOUND_ROWS()。但它在 MySQL 8.0+ 已被标记为废弃,且并发下不准、性能差。
- 替代方案:显式执行两次查询,第一次只
SELECT COUNT(*)加相同WHERE和GROUP BY逻辑 - 如果分组字段有索引,
COUNT(DISTINCT dept_id)可能比子查询快,但要注意 NULL 值是否计入 - 真实场景中,总数和分页数据的
WHERE条件必须完全一致,连参数绑定顺序都不能错,否则缓存或 ORM 易出错
分组分页的总数问题本质是语义歧义:数据库不知道你要的是“组的数量”“原始行数总和”还是“满足某条件的组内行数”。必须手动拆解清楚,没有银弹 SQL 能自动猜中。最容易被忽略的是 HAVING 对总数的影响——它筛掉的组,既不出现在分页结果里,也不该计入总数,但很多人忘了在总数查询里同步加这个条件。











