返回

解决Oracle数据库分页查询报错:实战指南与优化

java

Oracle 数据库分页查询,咋就这么难?

最近搞项目,数据库用的 Oracle,结果分页查询出了问题。 错误信息:org.springframework.dao.InvalidDataAccessResourceUsageException: JDBC exception executing SQL,一看就是 SQL 语法不对。 网上搜了一圈, 发现遇到这个问题的人还真不少。 这篇文章就来好好捋一捋 Oracle 分页这档子事。

一、 啥情况?

代码很简单,Spring Data JPA 标准的分页查询:

Pageable pageable = PageRequest.of(page, size);
Page<Rate> rates = rateRepository.findAll(pageable);

用的org.hibernate.dialect.OracleDialect。结果就报了上面的错。 报错信息里面关键的一句是:ORA-00933: SQL command not properly ended。 很明显, Hibernate 生成的 SQL 语句,Oracle 不认。

二、 咋回事?

根本原因在于,不同版本的 Oracle 数据库,支持的 SQL 语法不一样。 OracleDialect 对应的可能是比较老的 Oracle 版本(比如 Oracle 8i),它的分页语法和新版本(比如 Oracle 12c 及以上)有区别。

老版本的 Oracle 没有 OFFSET ... FETCH 这种标准的分页语法。 它用的是一种嵌套查询的方式来实现分页,类似于这样:

SELECT *
FROM   (SELECT a.*,
               ROWNUM rn
        FROM   (SELECT *
                FROM   your_table
                ORDER  BY some_column) a
        WHERE  ROWNUM <= :end_row)
WHERE  rn >= :start_row;

OFFSET ... FETCH 是 SQL 标准语法,从 Oracle 12c 才开始支持:

SELECT *
FROM   your_table
ORDER  BY some_column
OFFSET  :offset ROWS FETCH NEXT :limit ROWS ONLY;

Spring Data JPA 在使用 OracleDialect 时,可能生成了老版本的嵌套查询分页 SQL,但实际连接的数据库版本又比较新,导致语法错误。

三、 咋解决?

1. 明确指定 Oracle 版本对应的 Dialect

这是最推荐的做法。 找到你使用的 Oracle 数据库版本,然后选择对应的 Hibernate Dialect。

  • Oracle 12c 及以上:

    spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect
    
  • 其他的,自行查找对应版本的dialect,比如19c 23c

    如果没有完全匹配的 Dialect,可以找相近版本的。 例如,如果用的是 Oracle 19c,但没有 Oracle19cDialectOracle12cDialect 大概率也能用。

改完配置,重启应用,大概率就解决了。如果遇到NoClassDefFound, 请确认一下jar包是否正常导入。

2. 自定义查询(Native SQL 或 JPQL)

如果不想改 Dialect,或者有其他特殊需求,可以用 Native SQL 或 JPQL 自己写分页查询。

  • Native SQL:

    @Query(value = "SELECT * FROM ms_web_rate r ORDER BY r.created_at DESC OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY",
           countQuery = "SELECT count(*) FROM ms_web_rate",
           nativeQuery = true)
    Page<Rate> findRatesWithPagination(@Param("offset") int offset, @Param("limit") int limit);
    
     //调用
      Pageable pageable = PageRequest.of(page, size);
      Page<Rate> ratesPage= rateRepository.findRatesWithPagination(pageable.getPageNumber()*pageable.getPageSize(),pageable.getPageSize());
    

注意 : 用原生SQL时候要写countQuery 不然会报错,countQuery 用于获取总记录数,Spring Data JPA 需要这个信息来计算总页数。

  • JPQL (不推荐,但某些情况可能需要):

    JPQL 理论上也可以用 setFirstResultsetMaxResults 来实现分页,但我不推荐。因为 JPQL 的底层实现,还是会转换成具体的 SQL。 在 Oracle 下,它可能会转成效率较低的嵌套 ROWNUM 查询。

3. 升级 Hibernate(如果可以)

如果用的是很老的 Hibernate 版本,可以考虑升级。 新版本的 Hibernate 对 Oracle 的支持通常更好,可能会自动识别数据库版本,选择合适的 Dialect。 但是,直接升级hibernate,存在版本不兼容的风险。请测试完后再升级。

4. 使用 RowNum (不建议,除非是老版本 Oracle)

如果你的 Oracle 数据库版本确实很老,不支持 OFFSET ... FETCH,那只能用 ROWNUM 的嵌套查询方式。上面已经给了 SQL 示例。 可以在 Native SQL 中使用,或者用某种方式(比如自定义 Dialect)让 Hibernate 生成这种 SQL。

@Query(value = "SELECT * FROM (SELECT a.*, ROWNUM rn FROM (SELECT * FROM ms_web_rate ORDER BY created_at) a WHERE ROWNUM <= :endRow) WHERE rn > :startRow",
       countQuery = "SELECT count(*) FROM ms_web_rate",
       nativeQuery = true)
Page<Rate> findRatesWithRowNumPagination(@Param("startRow") int startRow, @Param("endRow") int endRow);

  //调用
  Pageable pageable = PageRequest.of(page, size);
  Page<Rate> rates = rateRepository.findRatesWithRowNumPagination(pageable.getPageNumber() * pageable.getPageSize(), (pageable.getPageNumber() + 1) * pageable.getPageSize());

这种做法性能不如 OFFSET ... FETCH,特别是在数据量大的时候。

四、其他注意点

  • 排序: 分页查询一定要有排序! 不然每次查询的结果顺序可能不一样,分页就乱套了。

  • 索引: 排序的字段要有索引! 不然查询会很慢。

  • 大偏移量: 如果偏移量(offset)很大,即使有索引,查询也可能很慢。 这种情况可以考虑用“游标”的方式,而不是直接跳到很大的偏移量。 这通常需要应用层做一些特殊处理。

进阶-游标分页(Keyset Pagination):
对于结果集非常庞大、且需要频繁进行大偏移量分页的场景,传统的分页方法(无论是ROWNUM还是OFFSET...FETCH)效率都会下降。此时可以考虑Keyset Pagination(也称为Seek Method)。
原理: 不直接跳过指定数量的行,而是记录上一页最后一条数据的“键值”(通常是排序字段的值),下一页查询时使用该键值作为筛选条件,获取比该键值“更大”(或“更小”,取决于排序方向)的若干条数据。
要求: 排序字段必须是唯一且有序的(例如,自增ID、创建时间等)。
示例:(假设按照created_at倒序排列)
java @Query(value = "SELECT * FROM ms_web_rate r WHERE r.created_at < :lastCreatedAt ORDER BY r.created_at DESC FETCH FIRST :limit ROWS ONLY", nativeQuery = true) List<Rate> findNextPageByCreatedAt(@Param("lastCreatedAt") LocalDateTime lastCreatedAt, @Param("limit") int limit);

第一页:

       List<Rate> firstPage=rateRepository.findNextPageByCreatedAt(LocalDateTime.MAX,pageSize);
  ```
  **后续页面:** 
    ```java
      LocalDateTime lastCreatedAt=firstPage.get(firstPage.size()-1).getCreatedAt();
      List<Rate> secondPage =rateRepository.findNextPageByCreatedAt(lastCreatedAt,pageSize);

这种分页效率只受limit影响,不受偏移量影响。

总结一下

Oracle 分页报错,大概率是 Dialect 没选对。选对 Dialect,问题基本解决。 如果有特殊需求,可以自定义 SQL。 但是,更建议优先尝试修改Dialect。 解决问题后, 多注意我提到的其他问题, 避免以后再踩坑。