SQL中如何分页查询分组聚合结果?
分页查询必须在分组之后做
分页查询必须在GROUP BY之后、ORDER BY之后执行,否则结果不可靠;正确顺序是WHERE→GROUP BY→HAVING→ORDER BY→LIMIT/OFFSET,且排序字段须出现在SELECT中。

不能直接对 GROUP BY 结果加 LIMIT,因为分组本身不保证顺序,且分页逻辑依赖于确定的行序。你得先完成分组聚合、排序,再分页——否则可能漏掉组、重复取组,或页与页之间数据错乱。
典型错误写法:SELECT sex, AVG(math) FROM stu GROUP BY sex LIMIT 0,2 —— 这看似“取前两组”,但 MySQL 不保证 GROUP BY 输出顺序,且没 ORDER BY 时结果不可预测,换次执行可能返回不同性别组。
- 必须显式加
ORDER BY(比如按平均分、人数、分组字段),否则LIMIT行为无意义 LIMIT和OFFSET是最后执行的步骤,只能作用于最终结果集,不是中间分组过程- 如果要跳过前 N 个分组,
OFFSET值是组数,不是原始行数
MySQL 8.0+ 推荐用 OFFSET + LIMIT 组合
这是最直观、兼容性好、语义清晰的方式。注意:排序字段必须出现在 SELECT 列表中,或至少是分组字段/聚合结果。
上面查第 1 页(每页 10 个分组);第 2 页就把
OFFSET改成10,依此类推。
分页注意事项与性能优化
在使用分页查询时,有几个关键点需要特别注意,以避免潜在的错误和性能瓶颈。
- 当请求的页码
OFFSET超出总分组数时,数据库会直接返回空结果集,而不会抛出错误。这种设计虽然安全,但开发者需确保前端逻辑能正确处理空数据。 - 性能隐患:当
OFFSET很大(比如几万),MySQL 仍需扫描并跳过前面所有分组,建议配合覆盖索引或改用游标分页。深度分页会导致大量的随机 I/O 和 CPU 计算,严重影响响应速度。 - 别用
LIMIT m,n旧写法(如LIMIT 0,10),它和LIMIT 10 OFFSET 0等价,但可读性差,且容易混淆起始位置。现代 SQL 标准推荐使用更直观的语法,以提高代码的可维护性。
HAVING 过滤后才能分页
想只对“人数 ≥ 2 的性别组”分页?必须把条件写在 HAVING,而不是 WHERE。因为 WHERE 在分组前过滤原始行,HAVING 才能基于聚合结果筛选分组。
理解这一区别至关重要。HAVING COUNT(*) >= 2 是合法的;WHERE COUNT(*) >= 2 会报错 Invalid use of group function。这是因为聚合函数不能在 WHERE 子句中直接使用,必须通过 HAVING 子句进行过滤。
HAVING过滤发生在分组和聚合之后、ORDER BY和LIMIT之前,所以分页对象是过滤后的分组集合。- 如果同时有
WHERE和HAVING,执行顺序是:WHERE → GROUP BY → 聚合 → HAVING → ORDER BY → LIMIT/OFFSET。务必遵循此顺序,否则可能导致结果不符合预期。
大数据量下分页聚合结果要小心
当分组数达到几千甚至上万,OFFSET 分页会越来越慢,因为 MySQL 每次都要重新执行完整分组、排序,再跳过前 N 组。这不是语法问题,是执行模型限制。
在这种情况下,传统的 LIMIT 分页方式效率极低。建议采用基于游标(Cursor)的分页方案,或者使用覆盖索引来减少回表操作。此外,可以考虑将聚合结果缓存到 Redis 等高速存储中,以应对高并发场景下的查询需求。
性能优化与常见误区
在实现高效分页时,需重点关注以下三个核心细节。首先,务必避免在 SELECT * 之后再执行 GROUP BY 操作。最佳实践是只选取真正需要的分组字段和聚合字段,这能显著减少内存占用及临时表的开销,从而提升查询效率。其次,确保 GROUP BY 字段具备合适的索引支持,尤其是复合索引应当完整覆盖 WHERE 条件、分组字段以及 ORDER BY 字段,以加速数据检索过程。最后,若需处理海量数据的分组页码,例如后台导出分页报表场景,建议采用游标分页策略。具体做法是记录上一页最后一个分组的排序值(如上页末尾的 avg_score),下一页通过 WHERE avg_score < 结合 LIMIT 进行定位,从而有效绕过 OFFSET 带来的性能瓶颈。
最易被忽略的关键点在于:分组分页的本质是对「聚合结果集」进行分页,而非对「原始表」直接分页。许多开发者错误地试图在子查询中先执行 LIMIT 再执行 GROUP BY,这种做法仅对部分数据进行了分组,导致最终的统计结果完全失真,无法反映真实业务数据。
SELECT sex, AVG(math) AS avg_score, COUNT(*) AS cnt FROM stu WHERE math > 60 GROUP BY sex ORDER BY avg_score DESC LIMIT 10 OFFSET 0;
SELECT sex, AVG(math) AS avg_score, COUNT(*) AS cnt FROM stu GROUP BY sex HAVING COUNT(*) >= 2 ORDER BY cnt DESC LIMIT 5 OFFSET 0;








