解决Oracle数据库分页查询报错:实战指南与优化
2025-03-07 10:29:56
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,但没有
Oracle19cDialect
,Oracle12cDialect
大概率也能用。
改完配置,重启应用,大概率就解决了。如果遇到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 理论上也可以用
setFirstResult
和setMaxResults
来实现分页,但我不推荐。因为 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。 解决问题后, 多注意我提到的其他问题, 避免以后再踩坑。