hibernate对oracle分页默认生成三层嵌套sql,最外层使用between导致无法下推过滤,引发全量加载和性能问题。

Oracle分页SQL被Hibernate错误包裹成三层嵌套
Hibernate默认对Oracle分页(如PageRequest.of(1, 20))生成的SQL,常是类似SELECT * FROM (SELECT a.*, ROWNUM rn FROM (/* your query */) a) WHERE rn BETWEEN ? AND ?这种三层结构。问题出在最外层的BETWEEN——Oracle无法将该条件“下推”到最内层查询,导致全量结果先被拉到JDBC驱动内存里,再由驱动做行号过滤。尤其当原始结果集有几万行时,光是ResultSet.next()遍历就吃掉大量CPU和GC压力。
验证方法:开启hibernate.show_sql=true和logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE,看实际执行的SQL是否含BETWEEN;再对比手写ROWNUM 两层写法的执行计划,观察<code>ROWS预估值差异。
- 强制用两层写法:改用原生SQL,内层
WHERE ROWNUM ,外层<code>WHERE ROWNUM_ >= :start - 升级Hibernate 5.4+并配置
hibernate.dialect=org.hibernate.dialect.Oracle12cDialect,它会优先用OFFSET ... FETCH语法(需Oracle 12c+) - 避免在
@Query中返回List<entity></entity>,改用List<object></object>或Stream减少映射开销
fetchSize未生效 + 可滚动ResultSet拖慢Oracle驱动
即使你写了jdbcTemplate.setFetchSize(50)或在SessionFactory配置了hibernate.jdbc.fetch_size,Hibernate仍可能忽略它——因为Oracle12cDialect以下版本默认创建ResultSet时没显式指定类型和并发模式,驱动被迫启用TYPE_SCROLL_INSENSITIVE,内部维护游标状态机和缓存缓冲区,每行读取都触发额外JNI调用和内存拷贝。
典型现象:Oracle AWR报告中sql_id对应SQL的elapsed_time不高,但cpu_time占比超70%,且DB time与application wait time差距大,说明瓶颈在客户端驱动解析而非数据库执行。
- 在
application.properties中追加:spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true,避免连接池干扰 - 自定义
ConnectionProvider,重写getConnection(),对Oracle连接调用setAutoCommit(false)后再prepareStatement - 手动设置JDBC参数:
connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)
Oracle CLOB字段触发流式读取 + 频繁BufferedReader.read()
当分页结果含CLOB列(比如日志内容、富文本),且使用旧版ojdbc6.jar或未禁用流式策略时,Oracle驱动会自动切换为逐字符流式读取。此时哪怕只查10条记录,ResultSet.getString("content")也会引发成千上万次BufferedReader.read()系统调用,CPU在用户态/内核态反复切换,top里看到java进程%sys持续飙高。
这不是Hibernate的问题,但Hibernate默认把所有字段都getString()出来——哪怕你根本不用那个CLOB字段。
- 升级驱动到
ojdbc8.jar(21.9+),并配置JVM启动参数:-Doracle.jdbc.useFetchSizeWithLongColumn=false - 在
resultMap或@Entity中,对CLOB字段用@Lob @Basic(fetch = FetchType.LAZY),确保不查就不加载 - 生产环境禁用
hibernate.format_sql=true,避免日志中打印CLOB内容导致额外字符串拼接
连接池maxLifetime过短导致驱动冷启动抖动
HikariCP默认maxLifetime=1800000ms(30分钟),而Oracle JDBC驱动内部有SQL解析缓存、类型转换器、字符集映射表等状态。频繁重建连接会让这些缓存反复失效,每次新连接首次执行分页SQL时,驱动都要重新编译SQL元信息、初始化CLOB处理器,表现为前几页慢、后续变快,重启服务后又复现。
这个抖动在分页场景下被放大:每页请求都可能命中不同连接,冷启动开销叠加映射逻辑,延迟毛刺明显。
- 将
hikari.max-lifetime设为5400000(90分钟)或干脆设为0(禁用) - 配合
hikari.idle-timeout=600000(10分钟),让空闲连接自然淘汰,而非强制销毁 - 在应用启动后,用健康检查SQL(如
SELECT 1 FROM DUAL)预热连接池,触发驱动初始化
真实分页慢,往往不是SQL写得差,而是Hibernate在Oracle上那几处默认行为和驱动细节叠在一起,把本可毫秒级完成的ROWNUM过滤,硬生生拖成了CPU密集型任务。重点盯住执行计划里的ROWS预估、驱动日志里的read()调用频次、连接池的连接重建率——这三个数字不对,优化就还在雾里。











