Navicat执行存储过程“快”实为默认预取大量结果(如1000行)并缓存,而程序因fetch size小、autocommit开启、绑定变量类型不匹配及无结果缓存,导致真实执行延迟放大。
Navicat 默认预取行数太高,程序却按需拉取
navicat 执行存储过程快,并不等于它真的“执行得快”,而是它默认一次性预取大量结果(如 1000 行),且只渲染前几页——你看到的“秒出”,其实是它在后台悄悄把整结果集都捞回来了。而程序(如 jdbc/odp.net)通常用 setfetchsize(10) 或默认小批量拉取,每次 fetch 都触发一次网络往返和 resultset 解析,对含游标或大量输出参数的存储过程,延迟会被显著放大。
- Navicat 的预取行为可在「工具 → 选项 → 数据库 → Oracle」里查到,默认
Fetch Size常为 1000 或更高;调大它会让首次显示变慢,但翻页更顺 - Java 程序中若用
PreparedStatement调用CALL my_proc(?, ?),必须显式设置statement.setFetchSize(100),否则驱动可能用 10 或更低值 - 尤其当存储过程返回 REF CURSOR 时,Oracle 服务端会按客户端 fetch size 分批构造结果集,fetch size 过小 = 多次执行相同逻辑路径 + 多次网络抖动
程序端未关闭自动提交,触发隐式事务开销
Navicat 默认以 autocommit=false 模式执行单条 CALL,整个调用在一个事务内完成;而很多程序框架(如 Spring Boot 默认配置)会在每个 @Transactional 方法外层加事务,或 JDBC Connection 初始化时就设为 autocommit=true。这会导致:存储过程内部的 DML 被额外包裹、日志刷盘更频繁、甚至引发不必要的锁等待。
- 检查程序中是否对存储过程调用加了
@Transactional注解,尤其是readOnly=false场景下毫无必要 - JDBC 层确认连接创建后是否执行过
conn.setAutoCommit(true);应改为false并由存储过程自身控制提交 - 若存储过程本身不含 COMMIT/ROLLBACK,务必确保调用它的 Connection 是
autoCommit=false,避免 Oracle 额外做事务状态同步
绑定变量类型与存储过程参数不匹配,引发隐式转换
这是最隐蔽也最常被忽略的一点:Navicat 传参时往往自动适配字段类型(比如把数字 123 当 NUMBER 传),而程序里若用 setString(1, "123") 给一个 IN p_id NUMBER 参数赋值,Oracle 就会在运行时把表字段 id 全部转成字符串比对,索引直接失效——哪怕执行计划看起来“走了索引”,实际是 INDEX FULL SCAN 或索引跳跃扫描。
- 用 SQL Trace 抓程序调用时的真实 bind 变量值:
binds => TRUE,对比 Navicat trace 中的P_ID: 123 (NUMBER)和程序 trace 中的P_ID: '123' (VARCHAR2) - Java 中改用
setLong(1, 123L)或setBigDecimal(1, BigDecimal.valueOf(123))显式匹配NUMBER类型 - PL/SQL Developer 或 SQL*Plus 中模拟程序行为:
EXEC my_proc('123');(带引号)复现慢现象,验证是否为类型问题
Navicat 启用了结果缓存,程序每次都是全新执行
Navicat 在「工具 → 选项 → 查询」里默认开启 Cache query results,对相同 CALL 语句(包括参数完全一致)会直接返回内存缓存结果;而程序每次 new PreparedStatement 都是全新上下文,无法命中任何客户端缓存——看起来是“执行慢”,其实是“没缓存”。但这仅影响重复调用,首次差异仍需归因于前述三点。
- 该缓存不跨会话、不持久,重启 Navicat 即失效;不能依赖它判断真实性能
- 若想在程序中模拟类似效果,需自行实现基于参数哈希的本地缓存(注意缓存穿透与失效策略)
- 真正要关注的是首次执行耗时,而非第 5 次——因为生产环境多数请求都是首次或低频参数
真正卡住的地方,往往不是存储过程逻辑本身,而是程序与 Oracle 之间那层薄薄的协议适配:fetch size 没对齐、autocommit 没关、类型没对上。这些细节在 Navicat 里被 GUI 自动抹平了,一到代码里就全暴露出来。











