导读:本期聚焦于阿里山老登创作的《Java项目中如何实现分页查询?数据库语句与逻辑实现方式详解》,敬请观看详情。分页查询几乎是所有Java Web项目都绕不开的功能,数据量一大就不可能一次性把全部记录加载到内存里。本文围绕Java项目中实现分页查询的几种常见思路展开,先从数据库层面讲清楚MySQL的limit语句和Oracle的rownum写法,再对比JDBC手工分页、MyBatis分页插件以及Spring Data JPA的PagingAndSortingRepository三种实现方式的优缺点,最后给出一个完整的分页请求参数校验和总页数计算的示例代码,并分析深分页场景下的性能优化技巧,帮助你根据项目技术栈选择合适的分页方案。

分页查询是Java后端开发中最常见的需求之一。无论是管理后台的用户列表,还是电商平台的商品展示,只要涉及大量数据的展示,就离不开分页。一次把几十万条记录全部查出来放到内存里,不仅会让接口响应变慢,还可能直接导致内存溢出。本文将从数据库语句和Java逻辑实现两个层面,详细讲解分页查询的完整实现过程。

Java项目中如何实现分页查询?数据库语句与逻辑实现方式详解

一、分页的底层原理与数据库分页语句

分页的本质是:根据当前页码和每页条数,计算出需要跳过的记录数,然后从数据库中只取出这一段数据。假设每页显示pageSize条,当前是第pageNum页,那么跳过的记录数就是(pageNum - 1) * pageSize。理解了这个公式,所有的分页实现都只是围绕它做封装。

不同数据库的分页语法差异较大。MySQL使用limit子句,这是最简洁的方式:

-- 跳过20条,取10条,即查询第3页(每页10条)
SELECT * FROM t_user ORDER BY id LIMIT 20, 10;

Oracle在12c之前的版本没有limit,需要借助rownum伪列来实现,写法上要嵌套一层子查询:

SELECT * FROM (
    SELECT t.*, ROWNUM rn FROM t_user t WHERE ROWNUM <= 30
) WHERE rn > 20;

需要注意的是,ROWNUM的过滤条件如果写成ROWNUM > 20放在外层是无效的,因为ROWUM是在取数据的过程中逐行赋值的,先过滤再编号,所以大于条件必须放在子查询外面,通过别名rn来判断。PostgreSQL的写法和MySQL类似,使用LIMIT 10 OFFSET 20。SQL Server则可以用OFFSET ... FETCH或者ROW_NUMBER()窗口函数。写分页SQL时一定要配合ORDER BY,否则数据库不保证返回顺序,翻页时可能出现重复或遗漏数据的问题。

二、JDBC手工分页的完整实现

在不使用任何框架的场景下,可以用原生JDBC实现分页。核心逻辑分三步:第一步校验并规范化分页参数,第二步执行count语句查询总记录数,第三步执行带limit的分页语句查出当前页数据。

public class PageResult<T> {
    private long total;        // 总记录数
    private int pageNum;       // 当前页码
    private int pageSize;      // 每页条数
    private int totalPages;    // 总页数
    private List<T> list;      // 当前页数据

    // 计算总页数:向上取整
    public int calcTotalPages() {
        if (pageSize <= 0) return 0;
        return (int) ((total + pageSize - 1) / pageSize);
    }
}

参数校验是不可省略的环节。前端传来的pageNum可能是0或者负数,pageSize可能传一个特别大的值甚至被恶意传入超大数字拖垮数据库,所以服务端必须做兜底:

int pageNum  = Math.max(1, paramMap.get("pageNum"));
int pageSize = paramMap.get("pageSize");
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100;  // 限制单页最大条数
long offset = (long)(pageNum - 1) * pageSize;

String sql = "SELECT id, username, email FROM t_user ORDER BY id LIMIT ?, ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setLong(1, offset);
ps.setLong(2, pageSize);
ResultSet rs = ps.executeQuery();

这里有一个容易踩的坑:offset的计算要用long类型。如果pageNum很大而pageSize也较大,(pageNum - 1) * pageSize用int相乘可能溢出变成负数,导致SQL报错。手工分页的好处是完全可控、没有框架依赖,适合小型项目或者学习原理,缺点是代码重复度高,每个列表接口都要写一遍类似的逻辑。

三、MyBatis与PageHelper插件分页

在MyBatis项目中,最流行的做法是引入PageHelper插件。它基于拦截器机制,在SQL执行前自动改写语句加上limit,并自动执行一条count查询,开发者只需要写普通的查询SQL即可。

先在pom.xml中加入依赖,然后在查询方法前调用PageHelper.startPage()

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.7</version>
</dependency>
// 紧跟其后的第一条查询会被自动分页
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectUserList(queryParam);

PageInfo<User> pageInfo = new PageInfo<>(users);
// pageInfo中包含total、pageNum、pageSize、pages、list等完整分页信息
return pageInfo;

使用PageHelper有几个注意点。第一,startPage()必须紧挨着查询语句,中间不能插入其他SQL操作,因为它是基于ThreadLocal实现的,如果后面的查询没有消费掉这个分页参数,参数会残留并影响到下一次查询。第二,如果列表查询带有复杂的left join,自动生成的count语句可能较慢,可以通过PageHelper.startPage(pageNum, pageSize, false)关闭count查询,或者手写优化后的count语句。第三,mapper中的SQL不需要写任何limit相关内容,保持单表纯净的查询条件,插件会自动拼接。

四、Spring Data JPA与深分页优化

如果项目使用Spring Data JPA,分页支持开箱即用。只需要在Repository接口中声明返回Page<T>类型的方法,并传入Pageable参数:

public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findByUsernameContaining(String keyword, Pageable pageable);
}

// 调用时构造Pageable,页码从0开始
PageRequest pageable = PageRequest.of(pageNum - 1, pageSize, Sort.by("id").descending());
Page<User> page = userRepository.findByUsernameContaining(keyword, pageable);
long total = page.getTotalElements();

需要特别留意JPA的页码是从0开始的,而前端习惯传从1开始的页码,转换时记得减一,这是实际项目中经常出现的off-by-one问题的来源。

最后谈谈深分页的性能问题。当翻到很靠后的页码时,例如LIMIT 1000000, 10,数据库实际上要先扫过前100万条记录再丢弃,越往后翻越慢。常见的优化方案有两种:一是利用上一页最后一条记录的id做游标式查询,写成WHERE id > lastId LIMIT 10,利用主键索引直接定位,速度几乎不受页码影响;二是先用覆盖索引查出当前页的id集合,再回表取完整数据:

SELECT u.* FROM t_user u
INNER JOIN (
    SELECT id FROM t_user ORDER BY id LIMIT 1000000, 10
) t ON u.id = t.id;

这种方式把大量扫描限制在索引范围内,回表只查10条记录,性能提升明显。对于无限滚动的信息流场景,推荐直接采用游标分页(cursor-based pagination),彻底放弃页码概念。

五、方案选型建议

综合来看,三种实现方式各有适用场景:原生JDBC适合理解原理和极简项目;MyBatis配合PageHelper是国内主流方案,灵活度高,对复杂SQL友好;Spring Data JPA适合快速开发的Crud类项目,代码量最少。无论选择哪种方案,都要记住三个原则:分页参数必须服务端校验并限制上限,查询必须带确定的排序字段,深分页场景要考虑游标优化。把这些细节处理好,分页功能才能既好用又稳定。

Java分页查询数据库分页PageHelper修改时间:2026-09-15 13:34:40

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260915/57301.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。