Java案例怎么分页查询数据库?

wen java案例 35

Java案例:高效分页查询数据库的完整指南与实战解析

目录导读

  1. 为什么分页查询很重要?
  2. 分页查询的核心原理与SQL实现
  3. 基于JDBC的传统分页案例
  4. 使用MyBatis实现优雅分页
  5. Spring Data JPA的分页最佳实践
  6. 常见分页问题与优化策略(含问答)
  7. 选择适合你的分页方案

为什么分页查询很重要?

在实际Java开发中,数据库表可能包含数百万甚至上亿条记录,如果一次性加载所有数据,不仅会占用大量内存,导致应用程序响应缓慢,还可能引发OOM(内存溢出)异常。分页查询通过按需加载数据,只返回当前页的记录,是提升系统性能的必备技能。

Java案例怎么分页查询数据库?

常见场景:电商商品列表、后台管理系统的数据表格、社交媒体的动态流等。


分页查询的核心原理与SQL实现

分页的本质是通过SQL语句限制返回的行数和偏移量,大多数关系型数据库都提供了分页语法:

  • MySQL:使用 LIMIT offset, sizeLIMIT size OFFSET offset
  • PostgreSQLLIMIT size OFFSET offset
  • Oracle:使用 ROWNUMFETCH NEXT(12c+)
  • SQL Server:使用 OFFSET-FETCHROW_NUMBER()

基础SQL示例(MySQL):

SELECT * FROM user ORDER BY id LIMIT 10, 20;

含义:跳过前10条,获取接下来的20条,即第2页(每页20条)。

注意ORDER BY 是必须的,否则分页结果可能不一致。


基于JDBC的传统分页案例

在纯JDBC中,我们可以使用 PreparedStatement 动态设置参数:

public List<User> getUsersByPage(int pageNum, int pageSize) {
    List<User> users = new ArrayList<>();
    int offset = (pageNum - 1) * pageSize;
    String sql = "SELECT * FROM user ORDER BY id LIMIT ?, ?";
    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setInt(1, offset);
        ps.setInt(2, pageSize);
        ResultSet rs = ps.executeQuery();
        while (rs.next()) {
            User user = new User();
            // 字段映射
            users.add(user);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return users;
}

缺点:代码冗余,硬编码SQL,不易维护。


使用MyBatis实现优雅分页

MyBatis提供了多种分页方式,最推荐的是 PageHelper 插件。

集成PageHelper步骤:

  1. 添加依赖(Maven/Gradle)
  2. 在MyBatis配置文件中添加插件
  3. 在Service层调用:
// 开启分页,只需这一行
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll(); // 原查询方法不变
PageInfo<User> pageInfo = new PageInfo<>(users);
// 获取分页信息
long total = pageInfo.getTotal(); // 总记录数
int pages = pageInfo.getPages();  // 总页数

原理:PageHelper通过拦截器自动将原SQL包装成两个SQL——一个用于查询总数(COUNT),一个用于分页查询(LIMIT)。

优势:无需修改原有的Mapper接口和XML,分页逻辑与业务完全解耦。


Spring Data JPA的分页最佳实践

Spring Data JPA内置了分页支持,通过 Pageable 接口实现。

代码示例:

// Repository接口
public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findAll(Pageable pageable);
}
// Service层调用
Pageable pageable = PageRequest.of(0, 20, Sort.by("id").ascending());
Page<User> userPage = userRepository.findAll(pageable);
List<User> content = userPage.getContent(); // 当前页数据
int totalPages = userPage.getTotalPages();  // 总页数
long totalElements = userPage.getTotalElements(); // 总记录数

高级用法:支持 Specification 动态查询:jpaSpecificationExecutor.findAll(spec, pageable)

注意:JPA的分页性能取决于实现,如果使用Hibernate,大偏移量时可能出现性能问题(如 OFFSET 很大),建议配合索引优化。


常见分页问题与优化策略(含问答)

Q1:为什么分页越往后查越慢?(深度分页问题)

A:当 LIMIT 100000, 20 时,数据库需要扫描前100020行,然后丢弃前100000行。OFFSET 越大,扫描的数据量越大。

解决方案

  • 游标分页(Keyset Pagination):基于上一页的最后一条记录的ID或时间戳。WHERE id > ? ORDER BY id LIMIT 20
  • 覆盖索引:确保 ORDER BYWHERE 涉及的字段在索引中。
  • 限制最大页数:产品层面限制用户只查看前100页,并用搜索替代翻页。

Q2:MyBatis PageHelper和JPA哪个性能更好?

A:两者性能接近,但PageHelper在复杂SQL(多表联查、子查询)时更灵活,因为它可以手动控制是否拦截,JPA适合简单CRUD,尤其在微服务中能快速开发。

Q3:分页查询会不会总耗时过高?

A:分页查询通常包含两次SQL(count查询 + 数据查询),如果表数据量极大,COUNT(*) 可能较慢,可以使用预估总数 + 实际数据的方式,或者使用不精确计数(如 EXPLAIN 估算)。

Q4:如何避免分页带来的“数据重复”或“丢失”问题?

A:在排序字段中,必须包含一个唯一值(如主键ID),如果排序字段有重复值(如时间戳),同一页可能在两次请求中返回不同结果,建议:ORDER BY create_time, id

Q5:前端传入的pageSize过大怎么办?

A:后端必须做安全校验,限制最大pageSize(maxPageSize = 100),防止有人恶意拉取全量数据。


选择适合你的分页方案

方案 适用场景 优点 缺点
JDBC原生 学习原理或极简项目 无额外依赖 代码冗余,不易维护
MyBatis + PageHelper 大部分Java Web项目 简单易用,灵活兼容复杂SQL 需注意版本兼容
Spring Data JPA 微服务、Spring Boot为主的项目 开发效率高,天然支持分页 大偏移量查询性能差
游标分页(Keyset) 大数据量、实时性高的场景(如Feed流) 性能稳定,支持无限翻页 不支持随机跳页

核心建议

  • 优先使用 MyBatis + PageHelperSpring Data JPA
  • 如果数据量超过10万,务必考虑游标分页Elasticsearch等搜索引擎。
  • 始终在排序字段上建立索引,并限制最大页数。

本文所有示例代码均为独立撰写,结合业界通用实践与搜索引擎资料整理而成,如需转载,请保留原文出处,文中涉及的技术栈版本请以官方最新文档为准。

抱歉,评论功能暂时关闭!