In Oracle 10g I have a table that holds timestamps showing how long certain operations took. It has two timestamp fields: starttime and endtime. I want to find averages of t
Unfortunately Oracle does not support most functions with intervals. There are a number of workarounds for this, but they all have some kind of drawback (and notably, none are ANSI-SQL compliant).
The best answer (as @justsalt later discovered) is to write a custom function to convert the intervals into numbers, average the numbers, then (optionally) convert back to intervals. Oracle 12.1 and later support doing this using a WITH
block to declare a function:
with
function fn_interval_to_sec(i in dsinterval_unconstrained)
return number is
begin
return ((extract(day from i) * 24
+ extract(hour from i) )*60
+ extract(minute from i) )*60
+ extract(second from i);
end;
select numtodsinterval(avg(fn_interval_to_sec(endtime-starttime)), 'SECOND')
from timings;
If you are on 11.2 or earlier, or if you prefer not to include functions in your SQL statements, you can declare it as a stored function:
create or replace function fn_interval_to_sec(i in dsinterval_unconstrained)
return number is
begin
return ((extract(day from i) * 24
+ extract(hour from i) )*60
+ extract(minute from i) )*60
+ extract(second from i);
end;
You can then use it in SQL as expected:
select numtodsinterval(avg(fn_interval_to_sec(endtime-starttime)), 'SECOND')
from timings;
dsinterval_unconstrained
Using the PL/SQL type alias dsinterval_unconstrained
for the function parameter ensures you have maximum precision/scale; INTERVAL DAY TO SECOND
defaults DAY
precision to 2 digits (meaning anything at or over ±100 days is an overflow and throws an exception) and SECOND
scale to 6 digits.
Additionally, Oracle 12.1 will raise a PL/SQL error if you try to specify any precision/scale in your parameter:
with
function fn_interval_to_sec(i in interval day(9) to second(9))
return number is
...
ORA-06553: PLS-103: Encountered the symbol "(" when expecting one of the following: to
Oracle supports custom aggregate functions written in PL/SQL, which would allow you to make minimal changes to the statement:
select ds_avg(endtime-starttime) from timings;
However, this approach has several major drawbacks:
avg
, as Oracle will always use the builtin avg
function rather than your own. (Technically you can, but then you have to qualify it with schema, which defeats the purpose.)If your values are not significantly far apart, @vadzim's approach works as well:
select avg((sysdate + (endtime-starttime)*24*60*60*1000000 - sysdate)/1000000.0)
from timings;
Be aware, though, that if the interval is too great, the (endtime-starttime)*24*60*60*1000000
expression will overflow and throw ORA-01873: the leading precision of the interval is too small
. At this precision (1μs) the difference cannot be greater than or equal to 00:16:40
in magnitude, so it is safe for small intervals, but not all.
Finally, if you are comfortable losing all subsecond precision, you can cast the TIMESTAMP
columns to DATE
; subtracting a DATE
from a DATE
will return the number of days with second precision (credit to @jimmyorr):
select avg(cast(endtime as date)-cast(starttime as date))*24*60*60
from timings;