如何在单次SQL查询中同时实现分页与总记录数统计?
这个问题我之前也遇到过!你原来的写法之所以不行,是因为聚合函数count(*)和普通的select *混用的时候,数据库不知道该怎么处理——不加GROUP BY的话,count(*)会把全表合并成一行统计,而select *是要返回多行数据,两者逻辑冲突,自然跑不起来。下面给你几个可行的解决办法:
方法一:执行两个独立查询(最常用、最可靠)
这是实际开发中最常用的方案,逻辑简单,也容易维护。先单独查询总记录数,再查询分页数据:
-- 第一步:获取总记录数(用于计算总页数) SELECT COUNT(*) AS total FROM `product`; -- 第二步:获取第1页的分页数据(每页20条) SELECT * FROM `product` LIMIT 20 OFFSET 0;
这种方式的优势在于,数据库优化器对这两个查询都能很好地处理,尤其是当product表有合适的索引时,COUNT(*)的执行速度会非常快。绝大多数ORM框架(比如MyBatis、Hibernate)的分页功能,底层都是用这种方式实现的。
方法二:用子查询将总记录数嵌入每一行
如果你的业务场景要求必须一次查询返回所有需要的数据,可以用子查询把总记录数作为一个额外字段返回:
SELECT p.*, (SELECT COUNT(*) FROM `product`) AS total FROM `product` p LIMIT 20 OFFSET 0;
这样返回的每一条分页数据都会带上total字段,虽然有点冗余,但能满足“一次查询拿到所有数据”的需求。需要注意的是,如果表的数据量特别大,COUNT(*)可能会有性能损耗,这时候如果业务允许,可以考虑用近似值(比如MySQL中通过EXPLAIN获取预估行数),但如果需要精确值,还是得用COUNT(*)。
方法三:用窗口函数(适合支持窗口函数的数据库)
如果你的数据库版本支持窗口函数(比如MySQL 8.0+、PostgreSQL、SQL Server等),可以用更优雅的写法:
SELECT *, COUNT(*) OVER() AS total FROM `product` LIMIT 20 OFFSET 0;
窗口函数COUNT(*) OVER()会在不聚合整个结果集的前提下,计算全表的总记录数,然后将这个值附加到每一行上。数据库会自动优化这个查询,不需要额外执行两次查询,写法也更简洁。
额外提示:优化大偏移量的分页性能
如果你的数据量很大,使用OFFSET进行分页可能会越来越慢(比如OFFSET 10000这样的查询,数据库需要先跳过前10000条数据)。这时候可以考虑用基于主键的分页,比如:
-- 假设product表有自增主键id,上一页最后一条数据的id是last_id SELECT * FROM `product` WHERE id > last_id LIMIT 20;
这种方式的性能会比OFFSET好很多,因为数据库可以直接通过主键索引定位到起始位置。
内容的提问来源于stack exchange,提问作者Renato Souza de Oliveira

