DBMS_UTILITY.GET_TIME 返回的是百分之一秒,不是毫秒
不少朋友在调试PL/SQL性能时,都踩过这个坑:想用dbms_utility.get_time来测量毫秒级耗时,结果发现两次调用相减的结果总是整数,数值跳变明显,甚至偶尔还会出现负数。这到底是怎么回事?其实,问题出在对这个函数返回单位的理解上。它返回的并非毫秒,而是「自数据库实例启动以来的百分之一秒(centisecond)」,也就是以10毫秒为单位的计数值。所以,它的最小分辨率就是10ms,而不是我们通常期望的1ms。

这意味着什么呢?简单来说,如果你的代码执行时间小于10毫秒,GET_TIME很可能返回相同的值,相减结果就是零。如果执行时间落在10到19毫秒之间,它只会显示1;20到29毫秒则显示2……本质上,它是向下取整到最近的10毫秒倍数。因此,有几点需要特别注意:
- 别用它来测量单条SQL或极短的PL/SQL代码块,误差会大到失去参考意义。
- 它更适合粗粒度的对比,比如评估两个存储过程的整体耗时差异是否超过了数十毫秒。
- 跨会话调用时要小心,虽然不同会话看到的时间基准一致,但系统时钟漂移或实例重启都会影响返回的绝对值。
正确计算耗时的写法:相减后乘 10 得到毫秒近似值
虽然无法获得精确的毫秒值,但只要理解了单位,进行合理换算就能得到可用的近似值。这里的关键,与其说是“怎么转换成毫秒”,不如说是“如何避免除法优先级导致的截断错误”和“怎样规避会话状态带来的干扰”。
一个典型的错误写法是:elapsed := end_time - start_time / 100;。这里由于除法优先级更高,会导致计算结果完全错乱。
- 正确的做法是必须先做减法,再乘以10:
elapsed_ms := (end_time - start_time) * 10; - 变量类型推荐使用
NUMBER或PLS_INTEGER,以避免隐式转换可能带来的精度损失。 - 如果想得到更易读的整数结果,可以四舍五入:
ROUND((end_time - start_time) * 10)。 - 需要牢记的是,这样计算出的值仍是近似值,实际误差范围在±5毫秒左右。
比 GET_TIME 更准的替代方案:DBMS_PROFILER 或 DBMS_SESSION
当真正需要定位毫秒级瓶颈时——比如想知道函数内部某一段逻辑具体卡在哪里——DBMS_UTILITY.GET_TIME就显得力不从心了。好在Oracle自带了一些更可靠的分析工具。
DBMS_PROFILER可以逐行统计代码的执行次数和耗时,不过需要经过开启、收集数据、分析结果三个步骤。- 可以用
DBMS_SESSION.SLEEP(0.01)配合GET_TIME来验证时间单位是否生效(注意,SLEEP操作不要在生产环境随意使用)。 - 对于Oracle 12c及以上的版本,推荐使用
DBMS_MONITOR结合ASH(Active Session History)数据,查询V$ACTIVE_SESSION_HISTORY视图中的TIME_WAITED字段,其单位是微秒,精度更高。 - 如果只是简单的脚本调试,其实不如直接使用SQL*Plus的
SET TIMING ON命令,它的底层调用了精度更高的计时器。
容易被忽略的陷阱:时区、RAC 节点、自治事务干扰
看起来只是一个简单的减法运算,但背后其实藏着几个容易踩中的隐蔽雷区。
- 在RAC(Real Application Clusters)环境下,不同节点的
GET_TIME基准时间可能存在微小偏差,因此跨节点进行计时比较并不建议。 - 在自治事务(
AUTONOMOUS_TRANSACTION)中调用GET_TIME,其返回值仍然遵循父会话的时间线,并不会为自治事务提供独立的计时。 - 数据库时间受操作系统时钟影响;如果数据库主机进行了NTP时间校正,
GET_TIME的返回值可能出现突变(虽然不常见,但在设置监控告警时需要留意)。 - 不要在循环中高频调用它——这个函数本身也有开销,频繁调用反而会扭曲被测代码的真实耗时。
说到底,要进行真正的性能压测或深度调优,必须结合AWR报告来分析DB Time和SQL执行计划,而不能仅仅依赖几个GET_TIME的测量点去猜测性能瓶颈所在。
